diff --git a/DDMsgReader.js b/DDMsgReader.js deleted file mode 100644 index 278b9fc7da67dd504f10294734976ecff8a7c0c6..0000000000000000000000000000000000000000 --- a/DDMsgReader.js +++ /dev/null @@ -1,24134 +0,0 @@ -/* 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) - * ... Comments trimmed ... - * 2023-09-22 Eric Oulashin Version 1.80 beta - * Improved speed of new-to-you scans, and to an extent (hopefully) overall speed - * Bug fix: Setting reverseListOrder to "ask" in the .cfg file works properly again. - * Bug fix: When listing messages in reverse order, the selected menu index - * (for lightbar mode) is now correct. - * Bug fix: If the user is allowed to read deleted messages, then allow - * the left & right arrow keys to to the next/previous message if it's deleted. - * Small fixes for indexed scanning mode. - * New: For personal email, unread emails will have an 'unread' message indicator - * in the message list as a U between the message number and the 'from' name. - * New user setting: "Quit from reader to message list": When enabled, - * quitting from reader mode goes to the message list instead of exiting - * out of DDMsgReader fully. - * New user setting: Enter/selection from indexed mode menu shows message list - * (instead of going into reader mode) - * New user setting: List messages in reverse order - * 2023-10-10 Eric Oulashin Version 1.80 - * Releasing this version - * 2023-10-11 Eric Oulashin Version 1.81 - * Updated permission check functions (speed improvement) - * 2023-10-18 Eric Oulashin Version 1.82 - * Fix for # posts and missing dates in sub-board list when changing sub-board - * 2023-10-25 Eric Oulashin Version 1.83 - * Personal emails to the sysop received as "sysop" (or starting with "sysop") - * are now correctly identified and marked as read when read - * 2023-10-26 Eric Oulashin Version 1.84 - * Fix in reader mode for refreshing the message area after - * closing another window (necessary with recent changes to - * substrWithAttrCodes()) - * 2023-11-01 Eric Oulashin Version 1.85 - * Mark personal email as read if the user is just reading personal email - * 2023-11-09 Eric Oulashin Version 1.86 - * New feature: For indexed mode, when choosing a sub-board, the R key - * can be used to mark all messages as read in the sub-board. - * Fix: For continuous newscan or browse newscan (SCAN_BACK), - * call the stock Synchronet behavior (DDMsgReader did this previously, - * as DDMsgReader doesn't implement those yet). - * Fix: In the message list, to-user alternate colors weren't being used - * unless the message was read. The correct colors are used again. - * 2023-11-09 Eric Oulashin Version 1.87 Beta - * Trying to speed things up by not getting vote headers all the time - * when calling get_all_msg_headers(), unless the vote headers are needed - * (when the sysop views who votes on a message when viewing tally info) - * New: User setting to only show new messages in a newscan (defaults to true/enabled) - * In the message list, there is now an additional space before the - * 'from' name, in case one of the status characters is a letter (this should - * look better). - * New: In lightbar mode, the indexed newscan menu can optionally 'snap' to - * the next sub-board with new messages when showing/returning to the menu - * Fix: When listing personal email, messages to the user were written - * with the to-user color wuen unread. Now the regular colors are always - * used (since all of a user's personal emails are 'to' them). - * Fix: For indexed newscan, if there are no sub-boards selected for scan - * in the user's newscan configuration, then output a message and return. - * Otherwise, it would end up in an infinite loop. - * Updated how user settings are loaded, to ensure that default user settings - * from DDMsgReader.cfg actually get set properly in the user settings. - * 2023-11-23 Eric Oulashin Version 1.88 - * New user setting/configuration option to prompt the user whether or - * not to delete a personal email after replying to it (defaults to false). - * New: Displays whether a personal email has been replied to. - * Fix: Now displaying message vote score in the default header again. - * Fix: When viewing message headers (for the sysop), now correctly - * shows the message attributes. - * 2023-11-30 Eric Oulashin Version 1.89 - * New: User option to toggle whether to display the email 'replied' indicator - * (defaults to true). - * Fix for setting colors for the key help lines so that the background - * won't get un-done if the other help line colors have a N (normal) attribute. - * 2023-12-02 Eric Oulashin Version 1.90 Beta - * New: operator menu for read mode, with the option to add the author to the - * twit list, etc. - * Fix: When refreshing a rectangular area of a message, if it's a poll message, - * the background color for the voted responses was used for the non-selected - * responses. - * Removed the setting useScrollingInterfaceForANSIMessages. - * 2023-12-04 Eric Oulashin Version 1.90 - * Releasing this version - * 2023-12-12 Eric Oulashin Version 1.90a - * New configurable colors in the theme file for the indexed mode newscan menu: - * indexMenuSeparatorLine (sub-board separator line) and indexMenuSeparatorText - * (sub-board separator text) - * 2023-12-15 Eric Oulashin Version 1.90b - * New configurable colors in the theme file for the indexed newscan menu - * header text (indexMenuHeader), "NEW" indicator text (indexMenuNewIndicator), - * and highlighted "NEW" indicator text (indexMenuNewIndicatorHighlight) - * 2023-12-26 Eric Oulashin Version 1.91 - * New sysop features while reading a message: Show message hex (with the X key) - * and save message hex to a file (with Ctrl-X) - * 2023-12-29 Eric Oulashin Version 1.92 - * Indexed newscan: By default, if there are no new messages, it now shows - * "No new messages." (578 QWKNoNewMessages from text.dat). There's a new user - * setting to toggle whether to use the indexed newscan menu even if there are - * no new messages. New configuration file option: displayIndexedModeMenuIfNoNewMessages, - * which is a default for the user setting. - * 2024-01-01 Eric Oulashin Version 1.93 - * New user-toggleable behavior: Show indexed menu after reading all new messages. - * Also, indexed reader mode (started with the -indexedMode command-line option) - * now lists ALL sub-boards, rather than only sub-boards the user has enabled - * for newscan. It also prompts the user to list sub-boards in the current group - * or all. - * 2024-01-04 Eric Oulashin Version 1.93a Beta - * Fix: For indexed read mode (not doing a newscan), when choosing a sub-board to - * read, the correct (first unread) message is displayed. Also, the user's scan - * pointer is also updated to the last_read pointer. - * 2024-01-07 Eric Oulashin Version 1.93a - * New operator option for read mode: Add author email to email.can - * Releasing this version - */ - -"use strict"; - - -/* 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. -*/ - -// - For pageUp & pageDown, enable alternate keys: -// - When reading a message - scrollTextLines() -// - When listing messages -// - When listing message groups & sub-boards for sub-board selection -// - For sub-board area search: -// - Enable searching in traditional interface -// - Update the keys in the lightbar help line and traditional interface - -// This script requires Synchronet version 3.18 or higher (for mouse hotspot support). -// Exit if the Synchronet version is below the minimum. -if (system.version_num < 31800) -{ - var message = "\x01n\x01h\x01y\x01i* Warning:\x01n\x01h\x01w Digital Distortion Message Reader " - + "requires version \x01g3.18\x01w or\r\n" - + "higher of Synchronet. This BBS is using version \x01g" + system.version - + "\x01w. Please notify the sysop."; - console.crlf(); - console.print(message); - console.crlf(); - console.pause(); - exit(); -} - -require("sbbsdefs.js", "K_UPPER"); -require("text.js", "Email"); // Text string definitions (referencing text.dat) -require("utf8_cp437.js", "utf8_cp437"); -require("userdefs.js", "USER_UTF8"); -require("dd_lightbar_menu.js", "DDLightbarMenu"); -require("html2asc.js", 'html2asc'); -require("attr_conv.js", "convertAttrsToSyncPerSysCfg"); -require("graphic.js", 'Graphic'); -require("smbdefs.js", "SMB_POLL_ANSWER"); -load('822header.js'); -var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a'); -var hexdump = load('hexdump_lib.js'); - - -// Reader version information -var READER_VERSION = "1.93a"; -var READER_DATE = "2024-01-07"; - -// 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); -// 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; -// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017 -// use CTRL-P and CTRL-N for PageUp and PageDown, respectively. sbbsdefs.js -// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names -// in this script so that this script will work with Synchronet systems before -// and after the update containing those key definitions. -var KEY_PAGE_UP = CTRL_P; -var KEY_PAGE_DOWN = CTRL_N; -// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js -// for KEY_PAGEUP and KEY_PAGEDN in case they change -if (typeof(KEY_PAGEUP) === "string") - KEY_PAGE_UP = KEY_PAGEUP; -if (typeof(KEY_PAGEDN) === "string") - KEY_PAGE_DOWN = KEY_PAGEDN; - -// 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) -// These were added to sbbsdef.js around December 17, 2017: -//var KEY_PAGEUP ='\x10'; /* ctrl-p (Page Up) */ -//var KEY_PAGEDN ='\x0e'; /* ctrl-n (Page Down) */ - -// Characters for display -// Box-drawing/border characters: Single-line -var UPPER_LEFT_SINGLE = "\xDA"; -var HORIZONTAL_SINGLE = "\xC4"; -var UPPER_RIGHT_SINGLE = "\xBF"; -var VERTICAL_SINGLE = "\xB3"; -var LOWER_LEFT_SINGLE = "\xC0"; -var LOWER_RIGHT_SINGLE = "\xD9"; -var T_SINGLE = "\xC2"; -var LEFT_T_SINGLE = "\xC3"; -var RIGHT_T_SINGLE = "\xB4"; -var BOTTOM_T_SINGLE = "\xC1"; -var CROSS_SINGLE = "\xC5"; -// Box-drawing/border characters: Double-line -var UPPER_LEFT_DOUBLE = "\xC9"; -var HORIZONTAL_DOUBLE = "\xCD"; -var UPPER_RIGHT_DOUBLE = "\xBB"; -var VERTICAL_DOUBLE = "\xBA"; -var LOWER_LEFT_DOUBLE = "\xC8"; -var LOWER_RIGHT_DOUBLE = "\xBC"; -var T_DOUBLE = "\xCB"; -var LEFT_T_DOUBLE = "\xCC"; -var RIGHT_T_DOUBLE = "\xB9"; -var BOTTOM_T_DOUBLE = "\xCA"; -var CROSS_DOUBLE = "\xCE"; -// Box-drawing/border characters: Vertical single-line with horizontal double-line -var UPPER_LEFT_VSINGLE_HDOUBLE = "\xD5"; -var UPPER_RIGHT_VSINGLE_HDOUBLE = "\xB8"; -var LOWER_LEFT_VSINGLE_HDOUBLE = "\xD4"; -var LOWER_RIGHT_VSINGLE_HDOUBLE = "\xBE"; -// Other special characters -var DOT_CHAR = "\xF9"; -var CHECK_CHAR = "\xFB"; -var THIN_RECTANGLE_LEFT = "\xDD"; -var THIN_RECTANGLE_RIGHT = "\xDE"; - -var BLOCK1 = "\xB0"; // Dimmest block -var BLOCK2 = "\xB1"; -var BLOCK3 = "\xB2"; -var BLOCK4 = "\xDB"; // Brightest block -var MID_BLOCK = ascii(254); -var TALL_UPPER_MID_BLOCK = "\xFE"; -var UPPER_CENTER_BLOCK = "\xDF"; -var LOWER_CENTER_BLOCK = "\xDC"; - - -const ERROR_PAUSE_WAIT_MS = 1500; - -// 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; - -// Scan scopes -const SCAN_SCOPE_SUB_BOARD = 0; -const SCAN_SCOPE_GROUP = 1; -const SCAN_SCOPE_ALL = 2; - -// 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; -// Actions for indexed Mode -const INDEXED_MODE_SUBBOARD_MENU = 30; - -// Definitions for help line refresh parameters for error functions -const REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE = 0; - -// Message list sort types -const MSG_LIST_SORT_DATETIME_RECEIVED = 0; -const MSG_LIST_SORT_DATETIME_WRITTEN = 1; - -// Special value for passing to PopulateHdrsForCurrentSubBoard(): Start populating -// message headers from the scan pointer. -// This is a negative number so it won't get confuesd with a message header index. -const POPULATE_MSG_HDRS_FROM_SCAN_PTR = -1; -const POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS = -2; // Get all message headers even for a newscan - -// Misc. defines -var ERROR_WAIT_MS = 1500; -var SEARCH_TIMEOUT_MS = 10000; - -// A regular expression to check whether a string is an email address -var gEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -// A regular expression to check whether a string is a FidoNet email address -var gFTNEmailRegex = /^.*@[0-9]+:[0-9]+\/[0-9]+$/; -// An array of regular expressions for checking for ANSI codes (globally in a string & ignore case) -var gANSIRegexes = [ new RegExp(ascii(27) + "\[[0-9]+[mM]", "gi"), - new RegExp(ascii(27) + "\[[0-9]+(;[0-9]+)+[mM]", "gi"), - new RegExp(ascii(27) + "\[[0-9]+[aAbBcCdD]", "gi"), - new RegExp(ascii(27) + "\[[0-9]+;[0-9]+[hHfF]", "gi"), - new RegExp(ascii(27) + "\[[sSuUkK]", "gi"), - new RegExp(ascii(27) + "\[2[jJ]", "gi") ]; - -// 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 the avatar support file is available, and load is if so -var gAvatar = null; -if (file_exists(backslash(system.exec_dir) + "load/avatar_lib.js")) - gAvatar = load({}, "avatar_lib.js"); - -// User twitlist filename (and settings filename) -var gUserTwitListFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_twitlist"; -var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_Settings"; - -///////////////////////////////////////////// -// Script execution code - -// Parse the command-line arguments - -var gCmdLineArgVals = parseArgs(argv); -if (gCmdLineArgVals.exitNow) - exit(0); -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.attributes = "N"; - console.crlf(); - console.print("\x01cMessage search:"); - console.crlf(); - var allowedKeys = ""; - if (!gListPersonalEmailCmdLineOpt) - { - allowedKeys = "ANKFTYUS"; - console.print(" \x01g\x01hN\x01y = \x01n\x01cNew message scan"); - console.crlf(); - console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword"); - console.crlf(); - console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name"); - console.crlf(); - console.print(" \x01h\x01gT\x01y = \x01n\x01cTo name"); - console.crlf(); - console.print(" \x01h\x01gY\x01y = \x01n\x01cTo you"); - console.crlf(); - console.print(" \x01h\x01gU\x01y = \x01n\x01cUnread (new) messages to you"); - console.crlf(); - console.print(" \x01h\x01gS\x01y = \x01n\x01cScan for msgs to you"); - console.crlf(); - } - else - { - // Reading personal email - Allow fewer choices - allowedKeys = "KF"; - console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword"); - console.crlf(); - console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name"); - console.crlf(); - } - console.print(" \x01h\x01gA\x01y = \x01n\x01cAbort"); - console.crlf(); - console.print("\x01n\x01cMake a selection\x01g\x01h: \x01c"); - // 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("\x01n\x01h\x01y\x01iAborted\x01n"); - console.crlf(); - console.pause(); - break; - } -} - -if (gDoDDMR) -{ - // Write the user's default twitlist if it doesn't already exist - writeDefaultUserTwitListIfNotExist(); - - // When exiting this script, make sure to set the ctrl key pasthru back to what it was originally - js.on_exit("console.ctrlkey_passthru = " + console.ctrlkey_passthru); - // Set a control key pass-thru so we can capture certain control keys that we normally wouldn't be able to - var gOldCtrlKeyPassthru = console.ctrlkey_passthru; // Backup to be restored later - console.ctrlkey_passthru = "+AGKLOPQRTUVWXYZ_"; - - // 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 (gCmdLineArgVals.indexedmode) - { - console.attributes = "N"; - console.crlf(); - console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: "); - var scopeChar = console.getkeys("GA").toString(); - if (typeof(scopeChar) === "string" && scopeChar != "") - { - var scanScope = SCAN_SCOPE_ALL; - if (scopeChar == "G") - scanScope = SCAN_SCOPE_GROUP; - else if (scopeChar == "G") - scanScope = SCAN_SCOPE_ALL; - msgReader.DoIndexedMode(scanScope, false); - } - else - { - console.putmsg(bbs.text(Aborted), P_SAVEATR); - console.pause(); - } - } - else - { - // 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; - if (msgReader.subBoardCode != "mail") - { - console.attributes = "N"; - console.crlf(); - console.print("Loading " + subBoardGrpAndName(msgReader.subBoardCode) + "...."); - console.line_counter = 0; // To prevent a pause before the message list comes up - } - msgReader.ReadOrListSubBoard(); - break; - case SEARCH_KEYWORD: - var txtToSearch = (gCmdLineArgVals.hasOwnProperty("searchtext") ? gCmdLineArgVals.searchtext : null); - var subBoardCode = (gCmdLineArgVals.hasOwnProperty("subboard") ? gCmdLineArgVals.subboard : null); - msgReader.SearchMsgScan("keyword_search", txtToSearch, subBoardCode); - 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.putmsg(msgReader.text.newMsgScanText); - } - 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.putmsg(msgReader.text.newToYouMsgScanText); - } - 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.putmsg(msgReader.text.allToYouMsgScanText); - } - 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 & ANSI temp directories if they exists - deltree(gFileAttachDir); - deltree(backslash(system.node_dir + "DDMsgReaderANSIMsgTemp")); - - // 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.attributes = "N"; -} - -exit(); -// End of script execution. Functions below: - -// Generates an internal enhanced reader header line for the 'To' user. -// -// Parameters: -// pColors: A JSON object containing the color settings read from the -// theme configuration file. This function will use the -// 'msgHdrToColor' or 'msgHdrToUserColor' property, depending -// on the pToReadingUser property. -// pToReadingUser: Boolean - Whether or not to generate the line with -// the color/attribute for the reading user -// -// Return value: A string containing the internal enhanced reader header -// line specifying the 'to' user -function genEnhHdrToUserLine(pColors, pToReadingUser) -{ - var toHdrLine = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 - + "\x01gT\x01n\x01go \x01h\x01c: " + - (pToReadingUser ? pColors.msgHdrToUserColor : pColors.msgHdrToColor) + - "@MSG_TO-L"; - var numChars = console.screen_columns - 21; - for (var i = 0; i < numChars; ++i) - toHdrLine += "#"; - toHdrLine += "@\x01k" + VERTICAL_SINGLE; - return toHdrLine; -} - -/////////////////////////////////////////////////////////////////////////////////// -// 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) -{ - // Set the methods for the object - this.setSubBoardCode = DigDistMsgReader_SetSubBoardCode; - this.RecalcMsgListWidthsAndFormatStrs = DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs; - this.NumMessages = DigDistMsgReader_NumMessages; - this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub; - this.PopulateHdrsForCurrentSubBoard = DigDistMsgReader_PopulateHdrsForCurrentSubBoard; - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard = DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard; - this.GetMsgIdx = DigDistMsgReader_GetMsgIdx; - 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.SearchMsgScan = DigDistMsgReader_SearchMsgScan; - this.RefreshHdrInSubBoardHdrs = DigDistMsgReader_RefreshHdrInSubBoardHdrs; - this.RefreshHdrInSavedArrays = DigDistMsgReader_RefreshHdrInSavedArrays; - 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.CreateLightbarMsgListMenu = DigDistMsgReader_CreateLightbarMsgListMenu; - this.CreateLightbarMsgGrpMenu = DigDistMsgReader_CreateLightbarMsgGrpMenu; - this.CreateLightbarSubBoardMenu = DigDistMsgReader_CreateLightbarSubBoardMenu; - this.AdjustLightbarMsgListMenuIdxes = DigDistMsgReader_AdjustLightbarMsgListMenuIdxes; - 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.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced; - this.ReadMessageEnhanced_Scrollable = DigDistMsgReader_ReadMessageEnhanced_Scrollable; - this.ShowHdrOrKludgeLines_Scrollable = DigDistMsgReader_ShowHdrOrKludgeLines_Scrollable; - this.ShowVoteInfo_Scrollable = DigDistMsgReader_ShowVoteInfo_Scrollable; - this.ScrollableReaderNextReadableMessage = DigDistMsgReader_ScrollableReaderNextReadableMessage; - this.ScrollReaderDetermineClickCoordAction = DigDistMsgReader_ScrollReaderDetermineClickCoordAction; - this.ReadMessageEnhanced_Traditional = DigDistMsgReader_ReadMessageEnhanced_Traditional; - this.ShowReadModeOpMenuAndGetSelection = DigDistMsgReader_ShowReadModeOpMenuAndGetSelection; - this.CreateReadModeOpMenu = DigDistMsgReader_CreateReadModeOpMenu; - 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.EditExistingMessageOldWay = DigDistMsgReader_EditExistingMessageOldWay; - this.CanDelete = DigDistMsgReader_CanDelete; - this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg; - this.CanEdit = DigDistMsgReader_CanEdit; - this.CanQuote = DigDistMsgReader_CanQuote; - this.ReadConfigFile = DigDistMsgReader_ReadConfigFile; - this.ReadUserSettingsFile = DigDistMsgReader_ReadUserSettingsFile; - this.WriteUserSettingsFile = DigDistMsgReader_WriteUserSettingsFile; - // TODO: Is this.DisplaySyncMsgHeader even needed anymore? Looks like it's not being called. - 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.NonDeletedMessagesExist = DigDistMsgReader_NonDeletedMessagesExist; - this.HighestMessageNum = DigDistMsgReader_HighestMessageNum; - this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum; - this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum; - this.ParseMsgAtCodes = DigDistMsgReader_ParseMsgAtCodes; - this.ReplaceMsgAtCodeFormatStr = DigDistMsgReader_ReplaceMsgAtCodeFormatStr; - this.FindNextReadableMsgIdx = DigDistMsgReader_FindNextReadableMsgIdx; - 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.DisplayAreaChgHdr = DigDistMsgReader_DisplayAreaChgHdr; - this.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar; - this.UpdateEnhancedReaderScrollbar = DigDistMsgReader_UpdateEnhancedReaderScrollbar; - this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted; - this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser; - this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError; - this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo; - this.PromptAndDeleteOrUndeleteMessage = DigDistMsgReader_PromptAndDeleteOrUndeleteMessage; - this.PromptAndDeleteOrUndeleteSelectedMessages = DigDistMsgReader_PromptAndDeleteOrUndeleteSelectedMessages; - this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo; - this.GetMsgHdrFieldListText = DigDistMsgReader_GetMsgHdrFieldListText; - this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader; - this.ShowMsgHex_Scrolling = DigDistMsgReader_ShowMsgHex_Scrolling; - this.GetMsgHexInfo = DigDistMsgReader_GetMsgHexInfo; - this.SaveMsgHexDumpToFile = DigDistMsgReader_SaveMsgHexDumpToFile; - this.GetLastReadMsgIdxAndNum = DigDistMsgReader_GetLastReadMsgIdxAndNum; - this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx; - 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.DeleteOrUndeleteSelectedMessages = DigDistMsgReader_DeleteOrUndeleteSelectedMessages; - this.NumSelectedMessages = DigDistMsgReader_NumSelectedMessages; - this.ForwardMessage = DigDistMsgReader_ForwardMessage; - this.VoteOnMessage = DigDistMsgReader_VoteOnMessage; - this.HasUserVotedOnMsg = DigDistMsgReader_HasUserVotedOnMsg; - this.GetUpvoteAndDownvoteInfo = DigDistMsgReader_GetUpvoteAndDownvoteInfo; - this.GetMsgBody = DigDistMsgReader_GetMsgBody; - this.RefreshMsgHdrInArrays = DigDistMsgReader_RefreshMsgHdrInArrays; - this.WriteLightbarKeyHelpErrorMsg = DigDistMsgReader_WriteLightbarKeyHelpErrorMsg; - - // 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 the command-line - // arguments can specify another mode. - // Note: This isn't used for "indexed" reader mode (but maybe it should be in the future) - this.startMode = READER_MODE_LIST; - - // hdrsForCurrentSubBoard is an array that will be populated with the - // message headers for the current sub-board. - this.hdrsForCurrentSubBoard = []; - // hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers - // to their index to hdrsForCurrentSubBoard - this.msgNumToIdxMap = {}; - - // 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 = {}; - 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.doingMsgScan = false; // Set to true in MessageAreaScan() - this.doingNewscan = false; // Whether or not we're doing a newscan - - this.subBoardCode = bbs.cursub_code; // The message sub-board code - this.readingPersonalEmail = false; - - // Whether or not we're in indexed reader mode - this.indexedMode = false; - // For indexed reader mode, whether or not to enable caching the message - // header lists for performance - this.enableIndexedModeMsgListCache = true; - - // this.colors will be an array of colors to use in the message list - this.colors = getDefaultColors(); - this.readingPersonalEmailFromUser = false; - if (typeof(pSubBoardCode) == "string") - { - var subCodeLowerCase = pSubBoardCode.toLowerCase(); - if (subBoardCodeIsValid(subCodeLowerCase)) - { - this.setSubBoardCode(subCodeLowerCase); - 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. Only for - // regular reading, not for newscans etc. - this.promptToContinueListingMessages = false; - // Whether or not to prompt the user to confirm to read a message - this.promptToReadMessage = false; - // For enhanced reader mode (reading only, not for newscan, etc.): Whether or - // not to ask the user whether to post on the sub-board in reader mode after - // reading the last message instead of prompting to go to the next sub-board. - // This is like the stock Synchronet behavior. - this.readingPostOnSubBoardInsteadOfGoToNext = 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.MSGNUM_LEN = 5; - 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 - this.FROM_LEN = ((console.screen_columns * (15/console.screen_columns)).toFixed(0)) - 1; // - 1 to make room for the space after the message indicator character - this.TO_LEN = ((console.screen_columns * (15/console.screen_columns)).toFixed(0)) - 1; // - 1 to account for the spaces around the status character - //var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces - //this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0); - this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-8; // 8 to account for the spaces - // For showing message scores in the message list - this.SCORE_LEN = 4; - // Whether or not to show message scores in the message list: Only if the terminal - // is at least 86 characters wide and if vote functions exist in the running build - // of Synchronet - this.showScoresInMsgList = ((console.screen_columns >= 86) && (typeof((new MsgBase("mail")).vote_msg) === "function")); - if (this.showScoresInMsgList) - this.SUBJ_LEN -= (this.SCORE_LEN + 1); // + 1 to account for a space - - // 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; - - // 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; - - // this.text is an object containing text used for various prompts & functions. - this.text = { - scrollbarBGChar: BLOCK1, - scrollbarScrollBlockChar: BLOCK2, - goToPrevMsgAreaPromptText: "\x01n\x01c\x01hGo to the previous message area", - goToNextMsgAreaPromptText: "\x01n\x01c\x01hGo to the next message area", - newMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan", - newToYouMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan", - allToYouMsgScanText: "\x01c\x01hA\x01n\x01cll \x01hM\x01n\x01cessages \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hS\x01n\x01ccan", - goToMsgNumPromptText: "\x01n\x01cGo to message # (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c", - msgScanAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage scan \x01h\x01y\x01iaborted\x01n", - msgSearchAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage search \x01h\x01y\x01iaborted\x01n", - deleteMsgNumPromptText: "\x01n\x01cNumber of the message to be deleted (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c", - editMsgNumPromptText: "\x01n\x01cNumber of the message to be edited (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c", - searchingSubBoardAbovePromptText: "\x01n\x01cSearching (current sub-board: \x01b\x01h%s\x01n\x01c)", - searchingSubBoardText: "\x01n\x01cSearching \x01h%s\x01n\x01c...", - scanningSubBoardText: "\x01n\x01cScanning \x01h%s\x01n\x01c...", - noMessagesInSubBoardText: "\x01n\x01h\x01bThere are no messages in the area \x01w%s\x01b.", - noSearchResultsInSubBoardText: "\x01n\x01h\x01bNo messages were found in \x01w%s\x01b with the given search criteria.", - msgScanCompleteText: "\x01n\x01h\x01cM\x01n\x01cessage scan complete\x01h\x01g.\x01n", - invalidMsgNumText: "\x01n\x01y\x01hInvalid message number: %d", - readMsgNumPromptText: "\x01n\x01g\x01h\x01i* \x01n\x01cRead message #: \x01h", - msgHasBeenDeletedText: "\x01n\x01h\x01g* \x01yMessage #\x01w%d \x01yhas been deleted.", - noHdrLinesForThisMsgText: "\x01n\x01h\x01yThere are no header lines for this message.", - noKludgeLinesForThisMsgText: "\x01n\x01h\x01yThere are no kludge lines for this message.", - searchingPersonalMailText: "\x01w\x01hSearching personal mail\x01n", - searchTextPromptText: "\x01cEnter the search text\x01g\x01h:\x01n\x01c ", - fromNamePromptText: "\x01cEnter the 'from' name to search for\x01g\x01h:\x01n\x01c ", - toNamePromptText: "\x01cEnter the 'to' name to search for\x01g\x01h:\x01n\x01c ", - abortedText: "\x01n\x01y\x01h\x01iAborted\x01n", - loadingPersonalMailText: "\x01n\x01cLoading %s...", - msgDelConfirmText: "\x01n\x01h\x01yDelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure", - msgUndelConfirmText: "\x01n\x01h\x01yUndelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure", - delSelectedMsgsConfirmText: "\x01n\x01h\x01yDelete selected messages: Are you sure", - undelSelectedMsgsConfirmText: "\x01n\x01h\x01yUndelete selected messages: Are you sure", - msgDeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been marked for deletion.", - msgUndeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been unmarked for deletion.", - selectedMsgsDeletedText: "\x01n\x01cSelected messages have been marked for deletion.", - selectedMsgsUndeletedText: "\x01n\x01cSelected messages have been unmarked for deletion.", - cannotDeleteMsgText_notYoursNotASysop: "\x01n\x01h\x01wCannot delete message #\x01y%d \x01wbecause it's not yours or you're not a sysop.", - cannotDeleteMsgText_notLastPostedMsg: "\x01n\x01h\x01g* \x01yCannot delete message #%d. You can only delete your last message in this area.\x01n", - cannotDeleteAllSelectedMsgsText: "\x01n\x01y\x01h* Cannot delete all selected messages", - msgEditConfirmText: "\x01n\x01cEdit message #\x01h%d\x01n\x01c: Are you sure", - noPersonalEmailText: "\x01n\x01cYou have no messages.", - postOnSubBoard: "\x01n\x01gPost on %s %s" - }; - - - // 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; - - // Whether or not to pause (with a message) after doing a new message scan - this.pauseAfterNewMsgScan = true; - - // For the message area chooser header filename & maximum number of - // area chooser header lines to display - this.areaChooserHdrFilenameBase = "areaChgHeader"; - this.areaChooserHdrMaxLines = 5; - - // Some key bindings for enhanced reader mode - this.enhReaderKeys = { - reply: "R", - privateReply: "I", - editMsg: "E", - showHelp: "?", - postMsg: "P", - nextMsg: KEY_RIGHT, - previousMsg: KEY_LEFT, - firstMsg: "F", - lastMsg: "L", - showKludgeLines: "K", - showHdrInfo: "H", - showMsgList: "M", - chgMsgArea: "C", - userEdit: "U", - quit: "Q", - prevMsgByTitle: "<", - nextMsgByTitle: ">", - prevMsgByAuthor: "{", - nextMsgByAuthor: "}", - prevMsgByToUser: "[", - nextMsgByToUser: "]", - prevMsgByThreadID: "(", - nextMsgByThreadID: ")", - prevSubBoard: "-", - nextSubBoard: "+", - downloadAttachments: CTRL_A, - saveToBBSMachine: CTRL_S, - deleteMessage: KEY_DEL, - selectMessage: " ", - batchDelete: CTRL_D, - forwardMsg: "O", - vote: "V", - showVotes: "T", - closePoll: "!", - bypassSubBoardInNewScan: "B", - userSettings: CTRL_U, - validateMsg: "A", // Only if the user is a sysop - quickValUser: CTRL_Q, - threadView: "*", // TODO: Implement this - operatorMenu: CTRL_O, - showMsgHex: "X", - hexDump: CTRL_X - }; - //if (user.is_sysop) - // this.enhReaderKeys.validateMsg = "A"; - // Some key bindings for the message list (not necessarily all of them) - this.msgListKeys = { - deleteMessage: KEY_DEL, - undeleteMessage: "U", - batchDelete: CTRL_D, - editMsg: "E", - goToMsg: "G", - chgMsgArea: "C", - userSettings: CTRL_U, - quit: "Q", - showHelp: "?" - }; - // Keys for the indexed mode menu - this.indexedModeMenuKeys = { - quit: "Q", - showMsgList: "M", - markAllRead: "R", - help: "?", - userSettings: CTRL_U, - }; - - // Message status characters for the message list - this.msgListStatusChars = { - selected: CHECK_CHAR, - unread: "U", - replied: "<", - attachments: "A", - deleted: "*" - }; - - // Whether or not to display avatars - this.displayAvatars = true; - this.rightJustifyAvatar = true; - - // Message list sort option - this.msgListSort = MSG_LIST_SORT_DATETIME_RECEIVED; - - // Whether or not to convert Y-style MCI attribute codes to Synchronet attribute codes - this.convertYStyleMCIAttrsToSync = false; - - // Whether or not to prepend the subject for forwarded messages with "Fwd: " - this.prependFowardMsgSubject = true; - - // An index of a quick-validation set for the sysop to apply to a user when reading their message, or - // an invalid index (such as -1) to show a menu of quick-validation values - this.quickUserValSetIndex = -1; - - // For the sysop, whether to save all message headers when saving a message to the BBS - // PC. This could be a boolean (true/false) or the string "ask" to prompt every time - this.saveAllHdrsWhenSavingMsgToBBSPC = false; - - this.cfgFilename = "DDMsgReader.cfg"; - // Check the command-line arguments for a custom configuration file name - // before reading the configuration file. Defaults to the current user - // number, but can be set by a loadable module command-line argument. Also, - // only allow changing it if the current user is a sysop. - this.personalMailUserNum = user.number; - var scriptArgsIsValid = (typeof(pScriptArgs) == "object"); - if (scriptArgsIsValid) - { - if (pScriptArgs.hasOwnProperty("configfilename")) - this.cfgFilename = pScriptArgs.configfilename; - if (pScriptArgs.hasOwnProperty("usernum") && user.is_sysop) - this.personalMailUserNum = pScriptArgs.usernum; - } - this.userSettings = { - twitList: [], - // Whether or not to use the scrollbar in the enhanced message reader - useEnhReaderScrollbar: true, - // Whether or not to only show new messages when doing a newscan - newscanOnlyShowNewMsgs: true, - // Whether or not to use indexed mode for doing a newscan - useIndexedModeForNewscan: false, - // Whether or not the indexed mode sub-board menu should "snap" selection to sub-boards with new messages - // when the menu is shown - indexedModeMenuSnapToFirstWithNew: false, - // Whether to display the indexed mode newscan menu when there are no new messages - displayIndexedModeMenuIfNoNewMessages: true, - // Whether to show the indexed newscan menu after reading all new messages - showIndexedNewscanMenuAfterReadingAllNewMsgs: true, - // Whether or not to list messages in reverse order - listMessagesInReverse: false, - // Whether or not quitting from the reader goes to the message list (instead of exiting altogether) - quitFromReaderGoesToMsgList: false, - // Whether or not the enter key in the indexed newscan menu shows the message list (rather than going to reader mode) - enterFromIndexMenuShowsMsgList: false, - // When reading personal email, whether or not to propmt if the user wants to delete a message after replying to it - promptDelPersonalEmailAfterReply: false, - // Whether or not to display the 'replied' status character (for personal email) - displayMsgRepliedChar: true - }; - // Read the settings from the config file (some settings could set user settings) - this.cfgFileSuccessfullyRead = false; - this.ReadConfigFile(); - this.ReadUserSettingsFile(false); - // 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 = "\x01n" + this.colors.msgListMsgNumHighlightColor + this.colors.msgListHighlightBkgColor; - this.colors.msgListFromHighlightColor = "\x01n" + this.colors.msgListFromHighlightColor + this.colors.msgListHighlightBkgColor; - this.colors.msgListToHighlightColor = "\x01n" + this.colors.msgListToHighlightColor + this.colors.msgListHighlightBkgColor; - this.colors.msgListSubjHighlightColor = "\x01n" + this.colors.msgListSubjHighlightColor + this.colors.msgListHighlightBkgColor; - this.colors.msgListDateHighlightColor = "\x01n" + this.colors.msgListDateHighlightColor + this.colors.msgListHighlightBkgColor; - this.colors.msgListTimeHighlightColor = "\x01n" + this.colors.msgListTimeHighlightColor + this.colors.msgListHighlightBkgColor; - // Similar for the area chooser lightbar highlight colors - this.colors.areaChooserMsgAreaNumHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; - this.colors.areaChooserMsgAreaDescHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDescHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; - this.colors.areaChooserMsgAreaDateHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDateHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; - this.colors.areaChooserMsgAreaTimeHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaTimeHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; - this.colors.areaChooserMsgAreaNumItemsHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumItemsHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor; - // Similar for the enhanced reader help line colors - this.colors.enhReaderHelpLineGeneralColor = "\x01n" + this.colors.enhReaderHelpLineGeneralColor + this.colors.enhReaderHelpLineBkgColor; - this.colors.enhReaderHelpLineHotkeyColor = "\x01n" + this.colors.enhReaderHelpLineHotkeyColor + this.colors.enhReaderHelpLineBkgColor; - this.colors.enhReaderHelpLineParenColor = "\x01n" + this.colors.enhReaderHelpLineParenColor + this.colors.enhReaderHelpLineBkgColor; - // Similar for the lightbar message list help line colors - this.colors.lightbarMsgListHelpLineGeneralColor = "\x01n" + this.colors.lightbarMsgListHelpLineGeneralColor + this.colors.lightbarMsgListHelpLineBkgColor; - this.colors.lightbarMsgListHelpLineHotkeyColor = "\x01n" + this.colors.lightbarMsgListHelpLineHotkeyColor + this.colors.lightbarMsgListHelpLineBkgColor; - this.colors.lightbarMsgListHelpLineParenColor = "\x01n" + this.colors.lightbarMsgListHelpLineParenColor + this.colors.lightbarMsgListHelpLineBkgColor; - // Similar for the lightbar area chooser help line colors - this.colors.lightbarAreaChooserHelpLineGeneralColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineGeneralColor + this.colors.lightbarAreaChooserHelpLineBkgColor; - this.colors.lightbarAreaChooserHelpLineHotkeyColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + this.colors.lightbarAreaChooserHelpLineBkgColor; - this.colors.lightbarAreaChooserHelpLineParenColor = "\x01n" + 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) != "\x01n")) - this.text[prop] = "\x01n" + 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", ""); - this.tabReplacementText = format("%*s", this.numTabSpaces, ""); - - // Calculate the message list widths and format strings based on the current - // sub-board code and color settings. Start with a message # field length - // of 4 characters. This will be re-calculated later after message headers - // are loaded. - this.RecalcMsgListWidthsAndFormatStrs(4); - - // 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 = loadTextFileIntoArray("enhMsgHeader", 10); - // this.enhMsgHeaderLinesToReadingUser will be a copy of this.endMsgReaderLines - // but with the 'To' user line changed to highlight the name for messages to - // the logged-on reading user - this.enhMsgHeaderLinesToReadingUser = []; - // If the header file didn't exist, then populate the enhanced reader header - // array with default lines. - this.usingInternalEnhMsgHdr = (this.enhMsgHeaderLines.length == 0); - if (this.usingInternalEnhMsgHdr) - { - // 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 = "\x01n\x01h\x01c" + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c" - + HORIZONTAL_SINGLE + " \x01h@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 += "@\x01k"; - numChars = console.screen_columns - console.strlen(hdrLine1) - 4; - for (var i = 0; i < numChars; ++i) - hdrLine1 += HORIZONTAL_SINGLE; - hdrLine1 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h" - + HORIZONTAL_SINGLE + UPPER_RIGHT_SINGLE; - this.enhMsgHeaderLines.push(hdrLine1); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine1); - var hdrLine2 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2 - + BLOCK3 + "\x01gM\x01n\x01gsg#\x01h\x01c: " + this.colors.msgHdrMsgNumColor + "@MSG_NUM_AND_TOTAL-L"; - numChars = console.screen_columns - 32; - for (var i = 0; i < numChars; ++i) - hdrLine2 += "#"; - hdrLine2 += "@\x01n\x01c" + VERTICAL_SINGLE; - this.enhMsgHeaderLines.push(hdrLine2); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine2); - var hdrLine3 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 - + "\x01gF\x01n\x01grom\x01h\x01c: " + this.colors.msgHdrFromColor + "@MSG_FROM_AND_FROM_NET-L"; - numChars = console.screen_columns - 36; - for (var i = 0; i < numChars; ++i) - hdrLine3 += "#"; - hdrLine3 += "@\x01k" + VERTICAL_SINGLE; - this.enhMsgHeaderLines.push(hdrLine3); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine3); - this.enhMsgHeaderLines.push(genEnhHdrToUserLine(this.colors, false)); - this.enhMsgHeaderLinesToReadingUser.push(genEnhHdrToUserLine(this.colors, true)); - var hdrLine5 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3 - + "\x01gS\x01n\x01gubj\x01h\x01c: " + this.colors.msgHdrSubjColor + "@MSG_SUBJECT-L"; - numChars = console.screen_columns - 26; - for (var i = 0; i < numChars; ++i) - hdrLine5 += "#"; - hdrLine5 += "@\x01k" + VERTICAL_SINGLE; - this.enhMsgHeaderLines.push(hdrLine5); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine5); - var hdrLine6 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2 + BLOCK3 - + "\x01gD\x01n\x01gate\x01h\x01c: " + this.colors.msgHdrDateColor + "@MSG_DATE-L"; - //numChars = console.screen_columns - 67; - //Wed, 08 Mar 2023 19:06:37 - numChars = 26 - 11; - for (var i = 0; i < numChars; ++i) - hdrLine6 += "#"; - //hdrLine6 += "@\x01n\x01c" + VERTICAL_SINGLE; - hdrLine6 += "@ @MSG_TIMEZONE@\x01n"; - numChars = console.screen_columns - 42; - hdrLine6 += format("%*s", numChars, ""); // More correct than format("%" + numChars + "s", ""); - hdrLine6 += "\x01n\x01c" + VERTICAL_SINGLE; - this.enhMsgHeaderLines.push(hdrLine6); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine6); - var hdrLine7 = "\x01n\x01h\x01c" + BOTTOM_T_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c" - + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h\x01k"; - numChars = console.screen_columns - 8; - for (var i = 0; i < numChars; ++i) - hdrLine7 += HORIZONTAL_SINGLE; - hdrLine7 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h" - + HORIZONTAL_SINGLE + BOTTOM_T_SINGLE; - this.enhMsgHeaderLines.push(hdrLine7); - this.enhMsgHeaderLinesToReadingUser.push(hdrLine7); - } - else - { - // We loaded the enhanced message header lines from a custom file. - // Copy from this.enhMsgHeaderLines to this.enhMsgHeaderLinesToReadingUser - // but change any 'To:' line to highlight the 'to' username. - this.enhMsgHeaderLinesToReadingUser = this.enhMsgHeaderLines.slice(); - // Go through the header lines and ensure the 'To' line has a different - // color - for (var lineIdx = 0; lineIdx < this.enhMsgHeaderLinesToReadingUser.length; ++lineIdx) - this.enhMsgHeaderLinesToReadingUser[lineIdx] = syncAttrCodesToANSI(strWithToUserColor(this.enhMsgHeaderLinesToReadingUser[lineIdx], this.colors.msgHdrToUserColor)); - } - // 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.WriteGrpListHdrLine1 = DigDistMsgReader_WriteGrpListTopHdrLine1; - this.WriteSubBrdListHdrLine = DigDistMsgReader_WriteSubBrdListHdrLine; - this.SelectMsgArea = DigDistMsgReader_SelectMsgArea; - this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar; - this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional; - this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional; - this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional; - // Lightbar-specific methods - this.WriteMsgGroupLine = DigDistMsgReader_writeMsgGroupLine; - this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader; - this.GetMsgSubBoardLine = DigDistMsgReader_GetMsgSubBrdLine; - // 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; - this.ValidateMsg = DigDistMsgReader_ValidateMsg; - this.GetGroupNameAndDesc = DigDistMsgReader_GetGroupNameAndDesc; - this.DoUserSettings_Scrollable = DigDistMsgReader_DoUserSettings_Scrollable; - this.DoUserSettings_Traditional = DigDistMsgReader_DoUserSettings_Traditional; - this.RefreshMsgAreaRectangle = DigDistMsgReader_RefreshMsgAreaRectangle; - this.MsgHdrFromOrToInUserTwitlist = DigDistMsgReader_MsgHdrFromOrToInUserTwitlist; - // For indexed mode - this.DoIndexedMode = DigDistMsgReader_DoIndexedMode; - this.IndexedModeChooseSubBoard = DigDistMsgReader_IndexedModeChooseSubBoard; - this.CreateLightbarIndexedModeMenu = DigDistMsgReader_CreateLightbarIndexedModeMenu; - this.GetIndexedModeSubBoardMenuItemTextAndInfo = DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo; - this.MakeIndexedModeHelpLine = DigDistMsgReader_MakeIndexedModeHelpLine; - this.ShowIndexedListHelp = DigDistMsgReader_ShowIndexedListHelp; - - // printf strings for message group/sub-board lists - // Message group information (printf strings) - this.msgGrpListPrintfStr = "\x01n " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen - + "d " + this.colors.areaChooserMsgAreaDescColor + "%-" - + this.msgGrpDescLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor - + "%" + this.numItemsLen + "d"; - this.msgGrpListHilightPrintfStr = "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " " - + "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen - + "d \x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaDescHighlightColor + "%-" - + this.msgGrpDescLen + "s \x01n" + 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 - // For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead. \x1b[V seems to work though. - this.lightbarAreaChooserHelpLine = "\x01n" - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`END`" + KEY_END + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "#" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + "/" - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`F`F@" - + this.colors.lightbarAreaChooserHelpLineParenColor + ")" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + "irst pg, " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`L`L@" - + this.colors.lightbarAreaChooserHelpLineParenColor + ")" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ast pg, " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`CTRL-F`" + CTRL_F + "@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`/`/@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`N`N@" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Q`Q@" - + this.colors.lightbarAreaChooserHelpLineParenColor + ")" - + this.colors.lightbarAreaChooserHelpLineGeneralColor + "uit, " - + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`?`?@"; - var lbAreaChooserHelpLineLen = 72; - // Pad the lightbar key help text on either side to center it on the screen - // (but leave off the last character to avoid screen drawing issues) - var padLen = console.screen_columns - lbAreaChooserHelpLineLen - 1; - var leftPadLen = Math.floor(padLen/2); - var rightPadLen = padLen - leftPadLen; - this.lightbarAreaChooserHelpLine = this.colors.lightbarAreaChooserHelpLineGeneralColor - + format("%*s", leftPadLen, "") - + this.lightbarAreaChooserHelpLine - + this.colors.lightbarAreaChooserHelpLineGeneralColor - + format("%" + rightPadLen + "s", "") + "\x01n"; - - // 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 = []; - - // 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 = {}; - - // areaChangeHdrLines is an array of text lines to use as a header to display - // above the message area changer lists. - this.areaChangeHdrLines = loadTextFileIntoArray(this.areaChooserHdrFilenameBase, this.areaChooserHdrMaxLines); - - // pausePromptText is the text that will be used for some of the pause - // prompts. It's loaded from text.dat, but in case that text contains - // "@EXEC:" (to execute a script), this script will default to a "press - // a key" message. - this.pausePromptText = bbs.text(Pause); - if (this.pausePromptText.toUpperCase().indexOf("@EXEC:") > -1) - this.pausePromptText = "\x01n\x01c[ Press a key ] "; - - // Menu option values to use for the operator menu for reader mode - this.readerOpMenuOptValues = { - validateMsg: 0, - editAuthorUserAccount: 1, - showHdrLines: 2, - showKludgeLines: 3, - showTallyStats: 4, - showMsgHex: 5, - saveMsgHexToFile: 6, - quickValUser: 7, - addAuthorToTwitList: 8, - addAuthorEmailToEmailFilter: 9 - }; - - // For indexed mode, whether to set the indexed mode menu item index to 1 more when showing - // the indexed mode menu again (i.e., when the user wants to go to the next sub-board) - this.indexedModeSetIdxMnuIdxOneMore = false; -} - -// 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.toLowerCase() == "mail"); -} - -// For the DigDistMsgReader class: Populates the hdrsForCurrentSubBoard -// array with message headers from the current sub-board. Filters out -// messages that are deleted, unvalidated, private, and voting messages. -// -// Parameters: -// pStartIdx: Optional - The index of the first message to retrieve. Only used if pEndIdx is also valid. -// This can also be the value POPULATE_MSG_HDRS_FROM_SCAN_PTR, to signify that the headers -// should be populated starting from the scan pointer for the current sub-board. -// pEndIdx: Optional - One past the index of the last message to retrieve. Only used if pStartIdx is also valid. -function DigDistMsgReader_PopulateHdrsForCurrentSubBoard(pStartIdx, pEndIdx) -{ - if (this.subBoardCode == "mail") - { - this.hdrsForCurrentSubBoard = []; - this.msgNumToIdxMap = {}; - return; - } - - var tmpHdrs = null; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - // First get all headers in a temporary array, then filter them into - // this.hdrsForCurrentSubBoard. - var startIdxIsNumber = (typeof(pStartIdx) === "number"); - // If doing a newscan but we want to get all headers anyway, then do it. - if (this.doingNewscan && startIdxIsNumber && pStartIdx == POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS) - tmpHdrs = msgbase.get_all_msg_headers(true, false); // Include votes, don't expand fields - // If doing a newscan and the setting to only show new messages for a newscan is enabled, then - // only get messages from the user's scan pointer - else if (this.doingNewscan && (this.userSettings.newscanOnlyShowNewMsgs || (startIdxIsNumber && pStartIdx == POPULATE_MSG_HDRS_FROM_SCAN_PTR))) - { - - { - // Populate the list of headers starting at the scan pointer. - var startMsgIdx = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[this.subBoardCode].scan_ptr); - var endMsgIdx = msgbase.total_msgs; - tmpHdrs = {}; - for (var i = startMsgIdx+1; i < endMsgIdx; ++i) - { - //var msgHdr = msgbase.get_msg_header(true, i, false, false); // Don't expand fields, don't include votes - // TODO: I think we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - var msgHdr = msgbase.get_msg_header(true, i, false, true); // Don't expand fields, include votes - if (msgHdr != null) - tmpHdrs[msgHdr.number] = msgHdr; - } - } - } - // If pStartIdx & pEndIdx are valid, then use those - else if (startIdxIsNumber && pStartIdx >= 0 && typeof(pEndIdx) === "number" && pEndIdx <= msgbase.total_msgs) - { - tmpHdrs = {}; - for (var i = pStartIdx; i < pEndIdx; ++i) - { - // Get message header by index - var msgHdr = msgbase.get_msg_header(true, i, false, true); // Don't expand fields, include vutes - //var msgHdr = msgbase.get_msg_header(true, i, false, false); // Don't expand fields, don't include votes - if (msgHdr != null) - tmpHdrs[msgHdr.number] = msgHdr; - } - } - else - { - // Get all message headers - // Note: get_all_msg_headers() was added in Synchronet 3.16. DDMsgReader requires a minimum - // of 3.18, so we're okay to use it. - tmpHdrs = msgbase.get_all_msg_headers(true, false); // Include votes, don't expand fields - } - msgbase.close(); - } - - // Filter the headers into this.hdrsForCurrentSubBoard - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpHdrs, true); -} - -// For the DigDistMsgReader class: Takes an array of message headers in the current -// sub-board and filters them into this.hdrsForCurrentSubBoard and -// this.msgNumToIdxMap based on which messages are readable to the -// user. -// -// Parameters: -// pMsgHdrs: An array/object of message header objects -// pClearFirst: Boolean - Whether or not to empty this.hdrsForCurrentSubBoard -// and this.msgNumToIdxMap first. -function DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard(pMsgHdrs, pClearFirst) -{ - if (pClearFirst) - { - this.hdrsForCurrentSubBoard = []; - this.msgNumToIdxMap = {}; - } - if (pMsgHdrs == null) - return; - - var idx = (this.hdrsForCurrentSubBoard.length > 0 ? this.hdrsForCurrentSubBoard.length - 1 : 0); - for (var prop in pMsgHdrs) - { - // Only add the message header if the message is readable to the user - // and the from & to name isn't in the user's personal twitlist. - if (isReadableMsgHdr(pMsgHdrs[prop], this.subBoardCode) && !this.MsgHdrFromOrToInUserTwitlist(pMsgHdrs[prop])) - { - this.hdrsForCurrentSubBoard.push(pMsgHdrs[prop]); - this.msgNumToIdxMap[pMsgHdrs[prop].number] = idx++; - } - } - - // If the sort type is date/time written, then sort the message header - // array as such - if (this.msgListSort == MSG_LIST_SORT_DATETIME_WRITTEN) - { - this.hdrsForCurrentSubBoard.sort(sortMessageHdrsByDateTime); - // Re-populate this.msgNumToIdxMap to match the order of this.hdrsForCurrentSubBoard - this.msgNumToIdxMap = {}; - for (var idx = 0; idx < this.hdrsForCurrentSubBoard.length; ++idx) - this.msgNumToIdxMap[this.hdrsForCurrentSubBoard[idx].number] = idx; - } -} - -// For the DigDistMsgReader class: Gets the message offset (index) for a message, given -// a message header. Returns -1 on failure. The returned index is for the object's -// message header array(s), if populated, in the priority of search headers, then -// hdrsForCurrentSubBoard. If neither of those are populated, the offset of the header -// in the messagebase will be returned. -// -// Parameters: -// pHdrOrMsgNum: Can either be a message header object or a message number. -// -// Return value: The message index (or offset in the messagebase) -function DigDistMsgReader_GetMsgIdx(pHdrOrMsgNum) -{ - var msgNum = -1; - if (typeof(pHdrOrMsgNum) == "object") - msgNum = pHdrOrMsgNum.number; - else if (typeof(pHdrOrMsgNum) == "number") - msgNum = pHdrOrMsgNum; - else - return -1; - - if (typeof(msgNum) != "number") - return -1; - - var msgIdx = 0; - if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && - (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0)) - { - for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i) - { - if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == msgNum) - { - msgIdx = i; - break; - } - } - } - else if (this.hdrsForCurrentSubBoard.length > 0) - { - if (this.msgNumToIdxMap.hasOwnProperty(msgNum)) - msgIdx = this.msgNumToIdxMap[msgNum]; - else - { - msgIdx = absMsgNumToIdx(this.subBoardCode, msgNum); - if (msgIdx != -1) - this.msgNumToIdxMap[msgNum] = msgIdx; - } - } - else - msgIdx = absMsgNumToIdx(this.subBoardCode, msgNum); - return msgIdx; -} - -// For the DigDistMsgReader class: 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. -// pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true. -// pSubBoardCode: Optional - An internal sub-board code. If not specified, then -// this method will default to this.subBoardCode. -// pWriteHdrToMsgbase: Optional boolean - Whether or not to also save the message to the messagebase. Defaults to true. -function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, pApply, pSubBoardCode, pWriteHdrToMsgbase) -{ - if (typeof(pMsgIndex) != "number") - return; - - var applyAttr = (typeof(pApply) === "boolean" ? pApply : true); - var subCode = (typeof(pSubBoardCode) === "string" ? pSubBoardCode : this.subBoardCode); - var writeHdrToMsgbase = (typeof(pWriteHdrToMsgbase) === "boolean" ? pWriteHdrToMsgbase : true); - if (this.msgSearchHdrs.hasOwnProperty(subCode)) - { - var msgNum = pMsgIndex + 1; - if (typeof(pAttrib) != "undefined") - { - if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex)) - { - if (applyAttr) - this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr | pAttrib; - else - this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr ^ pAttrib; - if (writeHdrToMsgbase) - { - // Using applyAttrsInMsgHdrInMessagbase(), which loads the header without - // expanded fields and saves the attributes with that header. - var msgNumFromHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].number; - applyAttrsInMsgHdrInMessagbase(pSubBoardCode, msgNumFromHdr, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr); - } - } - } - else - { - var msgHeader = this.GetMsgHdrByIdx(pMsgIndex); - if (msgHeader != null && this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex)) - { - this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex] = msgHeader; - if (writeHdrToMsgbase) - { - var msgbase = new MsgBase(subCode); - if (msgbase.open()) - { - msgbase.put_msg_header(true, msgHeader.offset, msgHeader); - msgbase.close(); - } - } - } - } - } -} - -// For the DigDistMsgReader class: Refreshes a message header in the message header -// array for the current sub-board. -// -// 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. -// pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true. -function DigDistMsgReader_RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, pApply) -{ - if (typeof(pMsgIndex) != "number") - return; - - if ((pMsgIndex >= 0) && (pMsgIndex < this.hdrsForCurrentSubBoard.length)) - { - var applyAttr = (typeof(pApply) === "boolean" ? pApply : true); - if (applyAttr) - this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr | pAttrib; - else - this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr ^ pAttrib; - } -} - -// For the DigDistMsgReader class: Refreshes a message header in the saved message -// header arrays. -// -// 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. -// pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true. -// pSubBoardCode: Optional - An internal sub-board code. If not specified, then -// this method will default to this.subBoardCode. -// pWriteHdrToMsgbase: Optional boolean - Whether or not to also save the message to the messagebase. Defaults to true. -function DigDistMsgReader_RefreshHdrInSavedArrays(pMsgIndex, pAttrib, pApply, pSubBoardCode, pWriteHdrToMsgbase) -{ - var applyAttr = (typeof(pApply) === "boolean" ? pApply : true); - this.RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, applyAttr, pSubBoardCode, pWriteHdrToMsgbase); - this.RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, applyAttr); -} - -// 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. Or, this can be the a boolean false for scan -// search mode to scan through sub-boards while searching each of them. -// pScanScopeChar: Optional string with a character specifying "A" to scan all sub-boards, -// "G" for the current message group, or "S" for the user's current sub-board. -// If this is not specified, the current sub-board will be used. -// pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text) -// pSkipSubBoardScanCfgCheck: Optional boolean - Whether or not to skip the sub-board scan config check for -// each sub-board. Defaults to false. -function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanScopeChar, pTxtToSearch, pSkipSubBoardScanCfgCheck) -{ - var searchTextProvided = (typeof(pTxtToSearch) === "string" && pTxtToSearch != ""); - var skipSubBoardScanCfgCheck = (typeof(pSkipSubBoardScanCfgCheck) === "boolean" ? pSkipSubBoardScanCfgCheck : false); - // 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.attributes = "N"; - console.crlf(); - var subCode = ""; - if (typeof(pScanScopeChar) !== "string") - { - subCode = (typeof(pSubBoardCode) === "string" ? pSubBoardCode : this.subBoardCode); - if (subCode == "mail") - { - //console.print("\x01n" + replaceAtCodesInStr(this.text.searchingPersonalMailText)); - console.putmsg("\x01n" + this.text.searchingPersonalMailText); - } - else - { - var formattedText = format(this.text.searchingSubBoardAbovePromptText, subBoardGrpAndName(bbs.cursub_code)); - //console.print("\x01n" + replaceAtCodesInStr(formattedText) + "\x01n"); - console.putmsg("\x01n" + formattedText + "\x01n"); - } - console.crlf(); - } - // Output the prompt text to the user (for modes where a prompt is needed) - switch (this.searchType) - { - case SEARCH_KEYWORD: - if (!searchTextProvided) - { - //console.print("\x01n" + replaceAtCodesInStr(this.text.searchTextPromptText)); - console.putmsg("\x01n" + this.text.searchTextPromptText); - } - else - console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n"); - break; - case SEARCH_FROM_NAME: - if (!searchTextProvided) - { - //console.print("\x01n" + replaceAtCodesInStr(this.text.fromNamePromptText)); - console.putmsg("\x01n" + this.text.fromNamePromptText); - } - else - console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n"); - break; - case SEARCH_TO_NAME_CUR_MSG_AREA: - if (!searchTextProvided) - { - //console.print("\x01n" + replaceAtCodesInStr(this.text.toNamePromptText)); - console.putmsg("\x01n" + this.text.toNamePromptText); - } - else - console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n"); - 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) - { - if (searchTextProvided) - this.searchString = pTxtToSearch; - else - 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("\x01n" + replaceAtCodesInStr(this.text.abortedText)); - //console.crlf(); - console.putmsg("\x01n" + this.text.abortedText); - console.crlf(); - console.pause(); - return; - } - else - { - // If pScanScopeChar is a string, then do scan search mode. Otherwise, - // scan/search the current sub-board. - if (typeof(pScanScopeChar) === "string" && (pScanScopeChar === "S" || pScanScopeChar === "G" || pScanScopeChar === "A")) - { - var subBoardCodeBackup = this.subBoardCode; - var subBoardsToScan = getSubBoardsToScanArray(pScanScopeChar); - this.doingMsgScan = true; - var continueScan = true; - this.doingMultiSubBoardScan = (subBoardsToScan.length > 1); - // 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. - for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueScan && !console.aborted; ++subCodeIdx) - { - // Pause for a short moment to avoid causing CPU usage goign to 99% - mswait(10); - - subCode = subBoardsToScan[subCodeIdx]; - if (skipSubBoardScanCfgCheck || (msg_area.sub[subCode].can_read && ((msg_area.sub[subCode].scan_cfg & SCAN_CFG_NEW) == SCAN_CFG_NEW))) - { - // Force garbage collection to ensure enough memory is available to continue - js.gc(true); - // 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; - // let the user read the sub-board (and toggle betweeen reading and - // listing) - var readOrListRetObj = this.ReadOrListSubBoard(subCode, null, true, true, false, true, READER_MODE_READ); - console.attributes = "N"; - console.crlf(); - //if (this.SearchTypePopulatesSearchResults()) - // console.print("\x01n\r\nSearching..."); - console.line_counter = 0; - if (readOrListRetObj.stoppedReading) - break; - } - } - this.subBoardCode = subBoardCodeBackup; - if (console.aborted) - { - console.print("\x01n" + replaceAtCodesInStr(this.text.msgSearchAbortedText) + "\x01n"); - console.crlf(); - console.aborted = false; // So that the console.pause() a couple lines down will indeed pause - } - console.pause(); - } - else - this.ReadOrListSubBoard(subCode); - // Clear the search data so that subsequent listing or reading sessions - // don't repeat the same search - this.ClearSearchData(); - } - } -} - -// For the DigDistMsgReader class: Performs a message search scan through sub-boards. -// Prompts the user for Sub-board/Group/All, then inputs search text from the user, then -// reads/lists messages through the sub-boards, performing the search in each sub-board. -// -// 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 -// pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text) -// pSubCode: Optional - An internal code of a sub-board if scanning just one sub-board -function DigDistMsgReader_SearchMsgScan(pSearchModeStr, pTxtToSearch, pSubCode) -{ - if (typeof(pSearchModeStr) !== "string" || pSearchModeStr.length == 0) - return; - - // If the given sub-board code is valid, then use that and scan only in that - // sub-board. Otherwise, prompt the user for sub-board, group, or all, then - // call SearchMessages to do the search. - var scanScopeChar = ""; - var previousSubBoardCode = null; - if (typeof(pSubCode) === "string" && subBoardCodeIsValid(pSubCode)) - { - var previousSubBoardCode = this.subBoardCode; - this.subBoardCode = pSubCode; - scanScopeChar = "S"; - } - else - { - console.mnemonics(bbs.text(SubGroupOrAll)); - scanScopeChar = console.getkeys("SGAC").toString(); - } - if (pSubCode == "mail") // Searching personal email - this.SearchMessages(pSearchModeStr, pSubCode, null, pTxtToSearch, true); // Skip/ignore scan config checks - else if (scanScopeChar.length > 0) - this.SearchMessages(pSearchModeStr, null, scanScopeChar, pTxtToSearch, true); // Skip/ignore scan config checks - else - { - console.crlf(); - //console.print(replaceAtCodesInStr(this.text.msgScanAbortedText)); - //console.crlf(); - //console.putmsg(this.text.msgScanAbortedText); - console.crlf(); - console.pause(); - } - // Restore this.subBoardCode if necessary - if (typeof(previousSubBoardCode) === "string") - this.subBoardCode = previousSubBoardCode; -} - -// 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]; - } - this.msgSearchHdrs = {}; - } -} - -// 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. -// pPromptToGoNextIfNoResults: Optional boolean - Whether or not to prompt the user -// to go onto the next/previous sub-board if there are no -// search results in the current sub-board. Defaults to true. -// pInitialModeOverride: Optional (numeric) to override the initial mode in this -// function (READER_MODE_READ or READER_MODE_LIST). If not -// specified, defaults to this.startMode. -// pOutputSubBoardPopulationMsgs: Optional (boolean): Whether or not to output sub-board -// population messages -// -// 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, - pPromptToGoNextIfNoResults, - pInitialModeOverride, pOutputSubBoardPopulationMsgs) -{ - var 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("\x01n\x01h\x01yWarning: \x01wThe 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 the user doesn't have permission to read the current sub-board, then - // don't allow the user to read it. - if (this.subBoardCode != "mail") - { - if (!msg_area.sub[this.subBoardCode].can_read) - { - var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name); - console.print("\x01n" + errorMsg); - 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); - var outputPopulateHdrsMsgs = (typeof(pOutputSubBoardPopulationMsgs) === "boolean" ? pOutputSubBoardPopulationMsgs : true); - if (!this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(true, outputPopulateHdrsMsgs, pauseOnNoSearchResults)) - { - retObj.stoppedReading = false; - return retObj; - } - // If not searching, then populate the array of all readable headers for the - // current sub-board. - if (!this.SearchingAndResultObjsDefinedForCurSub()) - this.PopulateHdrsForCurrentSubBoard(); - - // 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.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; - // If an initial mode override was specified and is valid, then use it. - if (typeof(pInitialModeOverride) === "number" && (pInitialModeOverride == READER_MODE_READ || pInitialModeOverride == READER_MODE_LIST)) - readerMode = pInitialModeOverride; - // 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.GetLastReadMsgIdxAndNum(false).lastReadMsgIdx; // Used to be true - if ((selectedMessageOffset > -1) && (selectedMessageOffset < this.NumMessages() - 1)) - ++selectedMessageOffset; - } - else - selectedMessageOffset = 0; - } - else if (this.hdrsForCurrentSubBoard.length > 0) - { - selectedMessageOffset = this.GetMsgIdx(GetScanPtrOrLastMsgNum(this.subBoardCode)); - if (selectedMessageOffset < 0) - selectedMessageOffset = 0; - else if (selectedMessageOffset >= this.hdrsForCurrentSubBoard.length) - selectedMessageOffset = this.hdrsForCurrentSubBoard.length - 1; - } - 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, pPromptToGoNextIfNoResults); - // 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. - // List messages - otherRetObj = this.ListMessages(null, 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; - } - } - - console.clear("\x01n"); - - 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(); - var formattedText = ""; - if (this.readingPersonalEmail) - formattedText = format(this.text.loadingPersonalMailText, subBoardGrpAndName(this.subBoardCode)); - else - formattedText = format(this.text.searchingSubBoardText, subBoardGrpAndName(this.subBoardCode)); - formattedText = replaceAtCodesAndRemoveCRLFs(formattedText); - console.print("\x01n" + formattedText + "\x01n"); - } - var readingMailUserNum = user.is_sysop ? this.personalMailUserNum : user.number; - this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser, null, null, readingMailUserNum); - } - } - 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; - if (outputMessages) - { - console.attributes = "N"; - console.crlf(); - if (this.readingPersonalEmail) - { - //console.print(replaceAtCodesInStr(this.text.noPersonalEmailText)); - if (this.searchType == SEARCH_NONE) - console.putmsg(this.text.noPersonalEmailText); - else - printf("\x01n" + this.text.noSearchResultsInSubBoardText, "Personal E-Mail"); - } - else - { - var formattedText = ""; - if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) - formattedText = format(this.text.noSearchResultsInSubBoardText, subBoardGrpAndName(this.subBoardCode)); - else - formattedText = format(this.text.noMessagesInSubBoardText, subBoardGrpAndName(this.subBoardCode)); - //console.putmsg(formattedText); // Doesn't seem to be word-wrapping - formattedText = replaceAtCodesInStr(formattedText); - formattedText = word_wrap(formattedText, console.screen_columns-1, formattedText.length, false).replace(/\r|\n/g, "\r\n"); - console.print(formattedText); - } - 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. -// pOutputMessages: Optional boolean: Whether or not to output scan status messages. Defaults to true. -function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar, pOutputMessages) -{ - 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.attributes = "N"; - console.mnemonics(bbs.text(SubGroupOrAll)); - 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(replaceAtCodesInStr(this.text.msgScanAbortedText)); - console.putmsg(this.text.msgScanAbortedText); - console.crlf(); - console.pause(); - return; - } - } - - var outputMessages = (typeof(pOutputMessages) === "boolean" ? pOutputMessages : true); - - // 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); - } - - this.doingNewscan = pScanMode === SCAN_NEW; - - // If doing a newscan of all sub-boards, and the user has their setting for indexed mode - // for newscan enabled, then do that and return instead of the traditional newscan. - if (pScanCfgOpt === SCAN_CFG_NEW && pScanMode === SCAN_NEW && this.userSettings.useIndexedModeForNewscan) - { - var scanScope = SCAN_SCOPE_ALL; - if (scanScopeChar === "S") scanScope = SCAN_SCOPE_SUB_BOARD; - else if (scanScopeChar === "G") scanScope = SCAN_SCOPE_GROUP; - msgReader.DoIndexedMode(scanScope, true); - this.doingNewscan = false; - return; - } - - // 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(); - - // Create an array of internal codes of sub-boards to scan - var subBoardsToScan = getSubBoardsToScanArray(scanScopeChar); - // Scan through the sub-boards - this.doingMsgScan = true; - var continueNewScan = true; - var userAborted = false; - this.doingMultiSubBoardScan = (subBoardsToScan.length > 1); - for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueNewScan && !console.aborted; ++subCodeIdx) - { - // Force garbage collection to ensure enough memory is available to continue - js.gc(true); - // 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(subBoardsToScan[subCodeIdx]); // 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)) - { - // If we are to output status messages, then do so and - if (outputMessages) - { - // If we're running a new message scan, then output which sub-board that is currently being scanned - if (pScanMode == SCAN_NEW || pScanMode == SCAN_BACK || pScanMode == SCAN_UNREAD) - { - var statusText = format(this.text.scanningSubBoardText, subBoardGrpAndName(this.subBoardCode)); - console.print("\x01n" + replaceAtCodesAndRemoveCRLFs(statusText) + "\x01n"); - console.crlf(); - console.line_counter = 0; // Prevent pausing for screen output and when displaying a message - } - } - - var grpIndex = msg_area.sub[this.subBoardCode].grp_index; - var subIndex = msg_area.sub[this.subBoardCode].index; - // 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. - //var msgbase = new MsgBasesubBoardsToScan[subCodeIdx]); - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - //this.setSubBoardCode(subBoardsToScan[subCodeIdx]); // Needs to be set before getting the last read/scan pointer index - - // TODO: This commented-out section of code may be redundant and no longer necessary. - // Perhaps this should be refactored? GetScanPtrMsgIdx() and FindNextReadableMsgIdx() - // may be relying on the fact that we poulate an array of cached message headers, - // filtered where deleted & unreadable ones are removed, so scan_ptr and last_read may - // not be quite accurate when we're using our cached header arrays. - /* - // If the current sub-board contains only deleted messages, - // or if the user has already read the last message in this - // sub-board, then skip it. - var scanPtrMsgIdx = this.GetScanPtrMsgIdx(); - var readableMsgsExist = (this.FindNextReadableMsgIdx(scanPtrMsgIdx-1, true) > -1); - var userHasReadLastMessage = false; - if (this.subBoardCode != "mail") - { - // What if newest_message_header.number is invalid (e.g. NaN or 0xffffffff or > - // msgbase.last_msg)? - if (this.hdrsForCurrentSubBoard.length > 0) - { - if ((msg_area.sub[this.subBoardCode].last_read == this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number) || - (scanPtrMsgIdx == this.hdrsForCurrentSubBoard.length-1)) - { - userHasReadLastMessage = true; - } - } - } - if (!readableMsgsExist || userHasReadLastMessage) - { - if (msgbase != null) - msgbase.close(); - continue; - } - */ - - var scanPtrMsgIdx = this.GetScanPtrMsgIdx(); - // 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. - var totalNumMsgs = msgbase.total_msgs; - // If the user's scan_ptr for the sub-board isn't the 'last message' special value, - // then check the user's scan_ptr against the message number of the last readable - // message (i.e., the last message in the sub-board could be a vote header, which - // isn't readable) - var scanPtrBeforeLastReadableMsg = false; - if (!subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode)) - { - var lastReadableMsgHdr = getLastReadableMsgHdrInMsgbase(msgbase, this.subBoardCode); - if (lastReadableMsgHdr != null) - scanPtrBeforeLastReadableMsg = (msg_area.sub[this.subBoardCode].scan_ptr < lastReadableMsgHdr.number); - else - scanPtrBeforeLastReadableMsg = (msg_area.sub[this.subBoardCode].scan_ptr < msgbase.last_msg); - } - //if ((totalNumMsgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < totalNumMsgs-1))) - if (totalNumMsgs > 0 && scanPtrBeforeLastReadableMsg) - { - bbs.curgrp = grpIndex; - bbs.cursub = subIndex; - // For a newscan, start at index 0 if the user wants to only show new messages - // during a newscan; otherwise, start at the scan pointer message (the sub-board - // will have to be populated with all messages) - var startMsgIdx = 0; - if (!this.userSettings.newscanOnlyShowNewMsgs) - { - // Start at the scan pointer - startMsgIdx = scanPtrMsgIdx; - // If the message has already been read, then start at the next message - var tmpMsgHdr = this.GetMsgHdrByIdx(startMsgIdx); - if ((tmpMsgHdr != null) && (msg_area.sub[this.subBoardCode].last_read == tmpMsgHdr.number) && (startMsgIdx < this.NumMessages() - 1)) - ++startMsgIdx; - } - // 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, null, null, 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, null, null, 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 anyUnreadToUser = anyUnreadMsgsToUserWithMsgbase(msgbase, this.subBoardCode); - if (anyUnreadToUser) - { - var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false, null, null, 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 (msgbase != null) - msgbase.close(); - } - } - // Briefly wait, to prevent the CPU from reaching 99% usage - mswait(10); - } - this.doingMultiSubBoardScan = false; - - - // 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 ((msgbase != null) && msgbase.is_open) - msgbase.close(); - this.doingMultiSubBoardScan = false; - this.doingMsgScan = false; - - if (this.pauseAfterNewMsgScan) - { - console.crlf(); - if (userAborted || console.aborted) - { - console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanAbortedText) + "\x01n"); - if (console.aborted) - console.aborted = false; // So that the console.pause() several lines down will indeed pause - } - else - console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanCompleteText) + "\x01n"); - console.crlf(); - console.pause(); - } - - this.doingNewscan = false; -} - -// 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)) -// pPromptToGoToNextAreaIfNoSearchResults: Optional boolean - Whether or not to -// prompt the user to go to the next/previous sub-board -// when there are no search results -// -// 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, pPromptToGoToNextAreaIfNoSearchResults) -{ - var retObj = { - lastUserInput: "", - lastAction: ACTION_NONE, - stoppedReading: false, - 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("\x01n\x01h\x01yWarning: \x01wThe 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("\x01n\x01h\x01yWarning: \x01wThe 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 the user doesn't have permission to read the current sub-board, then - // don't allow the user to read it. - if (this.subBoardCode != "mail") - { - if (!msg_area.sub[this.subBoardCode].can_read) - { - var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name); - console.print("\x01n" + errorMsg); - console.pause(); - retObj.stoppedReading = true; - return retObj; - } - } - - var msgbase = new MsgBase(this.subBoardCode); - // If the message base was not opened, then output an error and return. - if (!msgbase.open()) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01h\x01y* \x01wUnable 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. - var numOfMessages = this.NumMessages(msgbase); - msgbase.close(); - if (numOfMessages == 0) - { - console.clear("\x01n"); - console.center("\x01n\x01h\x01yThere 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.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.GetLastReadMsgIdxAndNum().lastReadMsgIdx; - if (msgIndex == -1) - msgIndex = 0; - else if (msgIndex >= numOfMessages) - msgIndex = numOfMessages - 1; - } - - // If the current message index is for a message that has been - // deleted and the user is not able to read deleted messages, then find the next non-deleted message. - var testMsgHdr = this.GetMsgHdrByIdx(msgIndex); - // TODO: Should this really allow reading messages that are marked for deletion? - if ((testMsgHdr == null) || (((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs())) - { - // First try going forward - var readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, true); - // If a non-deleted message was not found, then try going backward. - if (readableMsgIdx == -1) - readableMsgIdx = this.FindNextReadableMsgIdx(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 (readableMsgIdx > -1) - msgIndex = readableMsgIdx; - else - { - console.clear("\x01n"); - console.center("\x01h\x01yThere 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("\x01n"); - // 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 not readable to the user, then go to the - // next/previous message - else if (readMsgRetObj.msgNotReadable) - { - // 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.FindNextReadableMsgIdx(msgIndex, false); - else - msgIndex = this.FindNextReadableMsgIdx(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) - { - if (this.SearchTypePopulatesSearchResults()) - console.print("\x01n\r\nLoading messages..."); - var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults); - 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) - { - if (this.SearchTypePopulatesSearchResults()) - console.print("\x01n\r\nLoading messages..."); - var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults); - 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 FindNextReadableMsgIdx() will increment it - // before searching in order to find the "next" message. - msgIndex = this.FindNextReadableMsgIdx(-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.FindNextReadableMsgIdx(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. - var oldSubBoardCode = this.subBoardCode; - this.SelectMsgArea(); - if (this.subBoardCode != oldSubBoardCode) - this.PopulateHdrsForCurrentSubBoard(); - 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 and the user is not able to read deleted messages, then find the next non-deleted message. - testMsgHdr = this.GetMsgHdrByIdx(msgIndex); - // TODO: Should this really allow reading deleted messages? - if ((testMsgHdr == null) || (((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs())) - { - // First try going forward - var readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, true); - // If a non-deleted message was not found, then try going backward. - if (readableMsgIdx == -1) - readableMsgIdx = this.FindNextReadableMsgIdx(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 (readableMsgIdx > -1) - msgIndex = readableMsgIdx; - else - { - if (this.NumMessages() != 0) - { - // There are messages, but none that are not deleted. - console.clear("\x01n"); - console.center("\x01h\x01yThere are no messages to display."); - console.crlf(); - console.pause(); - } - 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, pPromptToGoToNextAreaIfNoSearchResults); - retObj.stoppedReading = goToPrevRetval.shouldStopReading; - 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, pPromptToGoToNextAreaIfNoSearchResults); - 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 - { - // List messages - var listRetObj = this.ListMessages(null, 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)) - 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. -// 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, pAllowChgSubBoard) -{ - var retObj = { - lastUserInput: "", - 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("\x01n\x01h\x01yWarning: \x01wThe 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("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because no message\r\n"); - console.print("sub-board was specified. Please notify the sysop.\r\n\x01p"); - return retObj; - } - - // If the user doesn't have permission to read the current sub-board, then - // don't allow the user to read it. - if (this.subBoardCode != "mail") - { - if (!msg_area.sub[this.subBoardCode].can_read) - { - var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name); - console.print("\x01n" + errorMsg); - console.pause(); - 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) - { - console.clear("\x01n"); - console.center("\x01n\x01h\x01yThere are no messages to display.\r\n\x01p"); - 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(); - - // 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(pAllowChgSubBoard); - else - retObj = this.ListMessages_Traditional(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. -// -// Parameters: -// 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(pAllowChgSubBoard) -{ - var retObj = { - lastUserInput: "", - selectedMsgOffset: -1 - }; - - // If the user doesn't have permission to read the current sub-board, then - // don't allow the user to read it. - if (this.subBoardCode != "mail") - { - if (!msg_area.sub[this.subBoardCode].can_read) - { - var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name); - console.print("\x01n" + errorMsg); - 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; - - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - { - console.center("\x01n\x01h\x01yError: \x01wUnable to open the sub-board.\r\n\x01p"); - 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; - - this.RecalcMsgListWidthsAndFormatStrs(); - - // Clear the screen and write the header at the top - console.clear("\x01n"); - 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.userSettings.listMessagesInReverse) - retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == this.NumMessages()-1), lastScreen, allowChgSubBoard); - else - retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == 0), lastScreen, 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.userSettings.listMessagesInReverse) - { - 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.userSettings.listMessagesInReverse) - this.tradListTopMsgIdx -= this.tradMsgListNumLines; - else - this.tradListTopMsgIdx += this.tradMsgListNumLines; - } - // First page - else if (retvalObj.userInput == "F") - { - if (this.userSettings.listMessagesInReverse) - this.tradListTopMsgIdx = this.NumMessages() - 1; - else - this.tradListTopMsgIdx = 0; - } - // Last page - else if (retvalObj.userInput == "L") - { - if (this.userSettings.listMessagesInReverse) - { - 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" || retvalObj.userInput == this.msgListKeys.deleteMessage || retvalObj.userInput == '\x7f' || retvalObj.userInput == '\x08') - { - if (retvalObj.userInput == '\x08') - console.crlf(); - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(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.PromptAndDeleteOrUndeleteMessage(msgNum-1, null, true); - - // Refresh the top header on the screen for continuing to list - // messages. - console.clear("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - } - // E: Edit a message - else if (retvalObj.userInput == this.msgListKeys.editMsg) // "E" - { - if (this.CanEdit()) - { - var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(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) - { - // See if the current message header has our "isBogus" property and it's true. - // Only let the user edit the message if it's not a bogus message header. - // The message header could have the "isBogus" property, for instance, if - // it's a vote message (introduced in Synchronet 3.17). - var tmpMsgHdr = this.GetMsgHdrByIdx(msgNum-1); - if (tmpMsgHdr != null) - var returnObj = this.EditExistingMsg(msgNum-1); - else - { - console.print("\x01n\r\n\x01h\x01yThat message isn't editable.\x01n"); - console.crlf(); - console.pause(); - } - } - - // Refresh the top header on the screen for continuing to list - // messages. - console.clear("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - } - // G: Go to a specific message by # (place that message on the top) - else if (retvalObj.userInput == this.msgListKeys.goToMsg) - { - var msgNum = this.PromptForMsgNum(curpos, "\x01n" + replaceAtCodesInStr(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("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - // ?: Display help - else if (retvalObj.userInput == "?") - { - console.clear("\x01n"); - this.DisplayMsgListHelp(!this.readingPersonalEmail && allowChgSubBoard, true); - console.clear("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - // C: Change to another message area (sub-board) - else if (retvalObj.userInput == this.msgListKeys.chgMsgArea) // "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("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - } - } - // S: Select message(s) - else if (retvalObj.userInput == "S") - { - // Input the message number list from the user - console.print("\x01n\x01cNumber(s) of message(s) to select, (\x01hA\x01n\x01c=All, \x01hN\x01n\x01c=None, \x01hENTER\x01n\x01c=cancel)\x01g\x01h: \x01c"); - 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("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - // Ctrl-D: Batch delete (for selected messages) - else if (retvalObj.userInput == this.msgListKeys.batchDelete) // CTRL_D - { - console.attributes = "N"; - console.crlf(); - if (this.NumSelectedMessages() > 0) - { - // The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation - // to delete the message and then delete it if confirmed. - this.PromptAndDeleteOrUndeleteSelectedMessages(null, true); - - // In case all messages were deleted, if the user can't view deleted messages, - // show an appropriate message and don't continue listing messages. - //if (this.NumMessages() == 0) - if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs()) - { - 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. - - //msgbase.close(); - //msgbase = null; - //console.clear("\x01n"); - //console.center("\x01n\x01h\x01yThere 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("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - } - else - { - // There are no selected messages - console.print("\x01n\x01h\x01yThere are no selected messages."); - mswait(ERROR_PAUSE_WAIT_MS); - // Refresh the top header on the screen for continuing to list messages. - console.clear("\x01n"); - this.WriteMsgListScreenTopHeader(); - } - } - // User settings - else if (retvalObj.userInput == this.msgListKeys.userSettings) - { - /* - var continueOn = true; - var retvalObj = null; - var curpos = null; // Current character position - var lastScreen = false; - - this.RecalcMsgListWidthsAndFormatStrs(); - if (this.tradListTopMsgIdx == -1) - this.SetUpTraditionalMsgListVars(); - this.WriteMsgListScreenTopHeader(); - */ - var userSettingsRetObj = this.DoUserSettings_Traditional(); - retvalObj.userInput = ""; - //drawMenu = userSettingsRetObj.needWholeScreenRefresh; - // In case the user changed their twitlist, re-filter the messages for this sub-board - if (userSettingsRetObj.userTwitListChanged) - { - console.gotoxy(1, console.screen_rows); - console.crlf(); - console.print("\x01nTwitlist changed; re-filtering.."); - var tmpMsgbase = new MsgBase(this.subBoardCode); - if (tmpMsgbase.open()) - { - var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true); - tmpMsgbase.close(); - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true); - } - else - console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p"); - this.RecalcMsgListWidthsAndFormatStrs(); - if (this.tradListTopMsgIdx == -1) - this.SetUpTraditionalMsgListVars(); - // If there are still messages in this sub-board, and the message offset is beyond the last - // message, then adjust the top message index as necessary. - if (this.hdrsForCurrentSubBoard.length > 0) - { - if (this.tradListTopMsgIdx > this.hdrsForCurrentSubBoard.length) - this.tradListTopMsgIdx = this.hdrsForCurrentSubBoard.length - this.tradMsgListNumLines; - } - else - { - continueOn = false; - retObj.selectedMsgOffset = -1; - } - } - if (userSettingsRetObj.needWholeScreenRefresh) - this.WriteMsgListScreenTopHeader(); - } - else - { - // If a message has been selected, exit out of this input loop - // so we can return from this method - The calling method will - // call the enhanced reader method. - if (retObj.selectedMsgOffset >= 0) - continueOn = false; - } - } - } - - msgbase.close(); - - return retObj; -} -// For the DigDistMsgReader class: Performs the message listing, given a -// sub-board code. This verison uses a lightbar interface for message -// navigation. -// -// Parameters: -// 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(pAllowChgSubBoard) -{ - var retObj = { - lastUserInput: "", - selectedMsgOffset: -1 - }; - - // If the user doesn't have permission to read the current sub-board, then - // don't allow the user to read it. - if (this.subBoardCode != "mail") - { - if (!msg_area.sub[this.subBoardCode].can_read) - { - var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name); - console.print("\x01n" + errorMsg); - console.pause(); - return retObj; - } - } - - // 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\x01h\x01ySorry, an ANSI terminal is required for this operation.\x01n\x01w\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.RecalcMsgListWidthsAndFormatStrs(); - - 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); - // Mouse: console.print replaced with console.putmsg for mouse click hotspots - //console.print(pHelpLineText); - console.putmsg(pHelpLineText); // console.putmsg() can process @-codes, which we use for mouse click tracking - console.cleartoeol("\x01n"); - } - - // Clear the screen and write the header at the top - console.clear("\x01n"); - 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(); - } - - // Create a DDLightbarMenu for the message list and list messages - // and let the user choose one - var msgListMenu = this.CreateLightbarMsgListMenu(); - // numMessages & numMessagesPerPage are used with msgListMenu.CalcPageForItemAndSetTopItemIdx() when - // going to a specific message. They're optional, but it can be faster to get them just once instead of - // every time. - var numMessages = msgListMenu.NumItems(); - var numMessagesPerPage = msgListMenu.GetNumItemsPerPage(); - var msgHeader = null; - var drawMenu = true; - var continueOn = true; - while (continueOn) - { - var userChoice = msgListMenu.GetVal(drawMenu); - drawMenu = true; - var lastUserInputUpper = (typeof(msgListMenu.lastUserInput) == "string" ? msgListMenu.lastUserInput.toUpperCase() : msgListMenu.lastUserInput); - // If the user's last input is null, then something bad/weird must have - // happened, so don't continue the input loop. - if (lastUserInputUpper == null) - { - continueOn = false; - break; - } - this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx; - // If userChoice is a number, then it will be a message number for a message to read - if (typeof(userChoice) == "number") - { - // The user choice a message to read - this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx; - msgHeader = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, this.showScoresInMsgList); - // TODO: Is this commented-out code necessary? In indexed newscan mode, when reading a - // message, then switching to the list, then selecting a message to read, this is re-printing - // the message info line from the message list and it's appearing over the help line at the - // bottom of the screen. - //if (msgHeader != null) - // this.PrintMessageInfo(msgHeader, true, this.lightbarListSelectedMsgIdx+1); - console.gotoxy(this.lightbarListCurPos); // Make sure the cursor is still in the right place - if (msgHeader != null) - { - // 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 - + +(this.GetMsgIdx(msgHeader.number) + 1) - + this.colors.readMsgConfirmColor - + ": Are you sure"; - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - readMsg = console.yesno(sReadMsgConfirmText); - } - 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; - retObj.selectedMsgOffset = this.GetMsgIdx(msgHeader.number); - if (retObj.selectedMsgOffset < 0) - retObj.selectedMsgOffset = 0; - } - // Return from here so that the calling function can switch into - // reader mode. - continueOn = false; - return retObj; - } - 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) - { - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - } - // If userChoice is not a number, then it should be null in this case, - // and the user would have pressed one of the additional quit keys set - // up for the menu. So look at the menu's lastUserInput and do the - // appropriate thing. - else if ((lastUserInputUpper == this.msgListKeys.quit) || (lastUserInputUpper == KEY_ESC)) // Quit - { - continueOn = false; - retObj.lastUserInput = "Q"; // So the reader will quit out - } - // Numeric digit: The start of a number of a message to read - else if (lastUserInputUpper.match(/[0-9]/)) - { - // Put the user's input back in the input buffer to - // be used for getting the rest of the message number. - console.ungetstr(lastUserInputUpper); - // Move the cursor to the bottom of the screen and - // prompt the user for the message number. - console.gotoxy(1, console.screen_rows); - var userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, replaceAtCodesInStr(this.text.readMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false); - if (userInput > 0) - { - // See if the current message header has our "isBogus" property and it's true. - // Only let the user read the message if it's not a bogus message header. - // The message header could have the "isBogus" property, for instance, if - // it's a vote message (introduced in Synchronet 3.17). - //GetMsgHdrByIdx(pMsgIdx, pExpandFields) - var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false); - if (tmpMsgHdr != null) - { - // 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); - retObj.selectedMsgOffset = userInput - 1; - // Return from here so that the calling function can switch - // into reader mode. - return retObj; - } - 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 - writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThat's not a readable message.", ERROR_PAUSE_WAIT_MS, "\x01n", true); - } - - // If the user chose to continue listing messages, then re-draw - // the screen. - if (continueOn) - { - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - // DEL key: Delete a message - else if (lastUserInputUpper == this.msgListKeys.deleteMessage || lastUserInputUpper == '\x7f' || lastUserInputUpper == '\x08') - { - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - - // The PromptAndDeleteOrUndeleteMessage() method will prompt the user for confirmation - // to delete the message and then delete it if confirmed. - this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, true); - - // In case all messages were deleted, if the user can't view deleted messages, - // show an appropriate message and don't continue listing messages. - //if (this.NumMessages() == 0) - if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs()) - continueOn = false; - else - { - // There are still some messages to show, so refresh the screen. - // Refresh the header & help line. - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - } - // E: Edit a message - else if (lastUserInputUpper == this.msgListKeys.editMsg) - { - if (this.CanEdit()) - { - // See if the current message header has our "isBogus" property and it's true. - // Only let the user edit the message if it's not a bogus message header. - // The message header could have the "isBogus" property, for instance, if - // it's a vote message (introduced in Synchronet 3.17). - var tmpMsgHdr = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, false); - if (tmpMsgHdr != null) - { - // Ask the user if they really want to edit the message - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - // Let the user edit the message - //var returnObj = this.EditExistingMsg(tmpMsgHdr.offset); - var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx); - // Refresh the header & help line - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - else - drawMenu = false; // No need to re-draw the menu - } - // G: Go to a specific message by # (highlight or place that message on the top, or in the page if can't put it on top) - else if (lastUserInputUpper == this.msgListKeys.goToMsg) - { - // Move the cursor to the bottom of the screen and - // prompt the user for a message number. - console.gotoxy(1, console.screen_rows); - var userMsgNum = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + replaceAtCodesInStr(this.text.goToMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false); - if (userMsgNum > 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(userMsgNum) != 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 = userMsgNum - 1; - msgListMenu.selectedItemIdx = chosenMsgIndex; - if ((chosenMsgIndex < msgListMenu.NumItems()) && (chosenMsgIndex >= this.lightbarListTopMsgIdx)) - { - this.lightbarListSelectedMsgIdx = chosenMsgIndex; - msgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx; - msgListMenu.CalcPageForItemAndSetTopItemIdx(numMessagesPerPage, numMessages); - } - else - { - this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex; - msgListMenu.topItemIdx = this.lightbarListTopMsgIdx; - } - } - else - { - // The user entered an invalid message number - console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, userMsgNum)) + "\x01n"); - console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS); - } - } - - // Refresh the header & help lines - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - // C: Change to another message area (sub-board) - else if (lastUserInputUpper == this.msgListKeys.chgMsgArea) - { - 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; - if (chgSubRetval.succeeded) - { - console.attributes = "N"; - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_rows); - console.print("Loading..."); - this.PopulateHdrsForCurrentSubBoard(); - this.SetUpLightbarMsgListVars(); - } - } - // Update the lightbar list variables and refresh the header & help lines - if (continueOn) - { - console.clear("\x01n"); - // Adjust the menu indexes to ensure they're correct for the current sub-board - this.AdjustLightbarMsgListMenuIdxes(msgListMenu); - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - else - drawMenu = false; // No need to re-draw the menu - } - else if (lastUserInputUpper == this.msgListKeys.showHelp) // Show help - { - console.clear("\x01n"); - this.DisplayMsgListHelp(!this.readingPersonalEmail && allowChgSubBoard, true); - // Re-draw the message list header & help line before - // the menu is re-drawn - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - // Spacebar: Select a message for batch operations (such as batch - // delete, etc.) - else if (lastUserInputUpper == " ") - { - this.ToggleSelectedMessage(this.subBoardCode, this.lightbarListSelectedMsgIdx); - // Have the menu draw only the check character column in the - // next iteration - msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 }; - } - // Ctrl-A: Select/de-select all messages - else if (lastUserInputUpper == CTRL_A) - { - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - console.gotoxy(1, console.screen_rows); - - // Prompt the user to select All, None (un-select all), or Cancel - console.print("\x01n\x01gSelect \x01c(\x01hA\x01n\x01c)\x01gll, \x01c(\x01hN\x01n\x01c)\x01gone, or \x01c(\x01hC\x01n\x01c)\x01gancel: \x01h\x01g"); - 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); - // Have the menu draw only the check character column in the - // next iteration - msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 }; - } - else - drawMenu = false; // No need to re-draw the menu - - // Refresh the help line - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - // Ctrl-D: Batch delete (for selected messages) - else if (lastUserInputUpper == this.msgListKeys.batchDelete) // CTRL_D - { - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - if (this.NumSelectedMessages() > 0) - { - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - - // The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation - // to delete the message and then delete it if confirmed. - var delSuccessful = this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, true); - // If successfully deleted, have the menu draw only the check character column in the - // next iteration - if (delSuccessful) - { - this.selectedMessages = {}; - drawMenu = true; - } - - // In case all messages were deleted, if the user can't view deleted messages, - // show an appropriate message and don't continue listing messages. - //if (this.NumMessages() == 0) - if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs()) - continueOn = false; - else - { - // There are still messages to list, so refresh the header & help lines - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - else - { - // There are no selected messages - writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThere are no selected messages.", ERROR_PAUSE_WAIT_MS, "\x01n", true); - // Refresh the help line - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - } - // U: Undelete message(s) - else if (lastUserInputUpper == this.msgListKeys.undeleteMessage) - { - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.clearline(); - if (this.NumSelectedMessages() > 0) - { - // Multi-message undelete - this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, false); - } - else - { - // Single-message undelete - this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, false); - } - - // Refresh the header & help line. - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - else if (lastUserInputUpper == this.msgListKeys.userSettings) - { - var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { DisplayHelpLine(pReader.msgListLightbarModeHelpLine); }); - lastUserInputUpper = ""; - drawMenu = userSettingsRetObj.needWholeScreenRefresh; - // In case the user changed their twitlist, re-filter the messages for this sub-board - if (userSettingsRetObj.userTwitListChanged) - { - console.gotoxy(1, console.screen_rows); - console.crlf(); - console.print("\x01nTwitlist changed; re-filtering.."); - var tmpMsgbase = new MsgBase(this.subBoardCode); - if (tmpMsgbase.open()) - { - var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true); - tmpMsgbase.close(); - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true); - } - else - console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p"); - this.SetUpLightbarMsgListVars(); - msgListMenu = this.CreateLightbarMsgListMenu(); - drawMenu = true; - } - if (userSettingsRetObj.needWholeScreenRefresh) - { - this.WriteMsgListScreenTopHeader(); - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - else - msgListMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight); - } - // S: Sorting options - // TODO - else if (lastUserInputUpper == "S") - { - // Refresh the help line - DisplayHelpLine(this.msgListLightbarModeHelpLine); - } - } - this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx; - this.lightbarListTopMsgIdx = msgListMenu.topItemIdx; - - return retObj; -} -// For the DigDistMsgLister class: Creates & returns a DDLightbarMenu for -// performing the lightbar message list. -function DigDistMsgReader_CreateLightbarMsgListMenu() -{ - // Start & end indexes for the various items in each message list row - var msgListIdxes = { - msgNumStart: 0, - msgNumEnd: this.MSGNUM_LEN, - selectMarkStart: this.MSGNUM_LEN, - selectMarkEnd: this.MSGNUM_LEN+2, - }; - msgListIdxes.fromNameStart = this.MSGNUM_LEN + 2; - msgListIdxes.fromNameEnd = msgListIdxes.fromNameStart + +this.FROM_LEN + 1; - msgListIdxes.toNameStart = msgListIdxes.fromNameEnd; - msgListIdxes.toNameEnd = msgListIdxes.toNameStart + +this.TO_LEN + 1; - msgListIdxes.subjStart = msgListIdxes.toNameEnd; - msgListIdxes.subjEnd = msgListIdxes.subjStart + +this.SUBJ_LEN + 1; - if (this.showScoresInMsgList) - { - msgListIdxes.scoreStart = msgListIdxes.subjEnd; - msgListIdxes.scoreEnd = msgListIdxes.scoreStart + +this.SCORE_LEN + 1; - msgListIdxes.dateStart = msgListIdxes.scoreEnd; - } - else - msgListIdxes.dateStart = msgListIdxes.subjEnd; - msgListIdxes.dateEnd = msgListIdxes.dateStart + +this.DATE_LEN + 1; - msgListIdxes.timeStart = msgListIdxes.dateEnd; - msgListIdxes.timeEnd = console.screen_columns - 1; // msgListIdxes.timeStart + +this.TIME_LEN + 1; - var msgListMenuHeight = console.screen_rows - this.lightbarMsgListStartScreenRow; - var msgListMenu = new DDLightbarMenu(1, this.lightbarMsgListStartScreenRow, console.screen_columns, msgListMenuHeight); - msgListMenu.scrollbarEnabled = true; - msgListMenu.borderEnabled = false; - msgListMenu.SetColors({ - itemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumColor}, - {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor}, - {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromColor}, - {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToColor}, - {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjectColor}, - {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateColor}, - {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeColor}], - altItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListToUserMsgNumColor}, - {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor}, - {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListToUserFromColor}, - {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToUserToColor}, - {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListToUserSubjectColor}, - {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListToUserDateColor}, - {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListToUserTimeColor}], - selectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor}, - {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor}, - {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor}, - {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor}, - {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor}, - {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor}, - {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}], - altSelectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor}, - {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor}, - {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor}, - {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor}, - {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor}, - {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor}, - {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}] - }); - // If we are to show message vote scores in the list (i.e., if the - // user's terminal is wide enough), then splice in color specifiers - // for the score column. - if (this.showScoresInMsgList) - { - msgListMenu.colors.itemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreColor}); - msgListMenu.colors.altItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListToUserScoreColor}); - msgListMenu.colors.selectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor}); - msgListMenu.colors.altSelectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor}); - } - - msgListMenu.multiSelect = false; - msgListMenu.ampersandHotkeysInItems = false; - msgListMenu.wrapNavigation = false; - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - // Ctrl-A: Select all messages - var additionalQuitKeys = "EeqQgGcCsS ?0123456789" + CTRL_A + this.msgListKeys.batchDelete + this.msgListKeys.userSettings; - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - additionalQuitKeys += this.msgListKeys.deleteMessage + this.msgListKeys.undeleteMessage.toLowerCase() + this.msgListKeys.undeleteMessage.toUpperCase(); - additionalQuitKeys += '\x7f'; // Ensure DEL is in there - additionalQuitKeys += '\x08'; // Ensure BACKSPACE is in there (can be an alternate for delete) - } - if (this.CanEdit()) - additionalQuitKeys += this.msgListKeys.editMsg; - msgListMenu.AddAdditionalQuitKeys(additionalQuitKeys); - - // Add additional keypresses for PageUp, PageDown, HOME (first page), and END (last page) - msgListMenu.AddAdditionalPageUpKeys("Pp"); // Previous page - msgListMenu.AddAdditionalPageDownKeys("Nn"); // Next page - msgListMenu.AddAdditionalFirstPageKeys("Ff"); // First page - msgListMenu.AddAdditionalLastPageKeys("Ll"); // Last page - - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - msgListMenu.msgListIdxes = msgListIdxes; - msgListMenu.msgReader = this; // Add this object to the menu object - msgListMenu.NumItems = function() { - return this.msgReader.NumMessages(); - }; - msgListMenu.readingPersonalEmail = this.subBoardCode.toLowerCase() == "mail"; - msgListMenu.GetItem = function(pItemIndex) { - var menuItemObj = this.MakeItemWithRetval(-1); - var itemIdx = (this.msgReader.userSettings.listMessagesInReverse ? this.msgReader.NumMessages() - pItemIndex - 1 : pItemIndex); - // In order to get vote score information (displayed if the user's terminal is wide - // enough), the 2nd parameter to GetMsgHdrByIdx() should be true. - var msgHdr = this.msgReader.GetMsgHdrByIdx(itemIdx, this.msgReader.showScoresInMsgList); - if (msgHdr != null) - { - // When setting the item text, call PrintMessageInfo with true as - // the last parameter to return the string instead - menuItemObj.text = strip_ctrl(this.msgReader.PrintMessageInfo(msgHdr, false, itemIdx+1, true)); - menuItemObj.retval = msgHdr.number; - var msgIsToUser = userHandleAliasNameMatch(msgHdr.to); - var msgIsFromUser = userHandleAliasNameMatch(msgHdr.from); - var readingPersonalEmail = (this.msgReader.subBoardCode == "mail"); - if (!readingPersonalEmail) - menuItemObj.useAltColors = msgIsToUser; - // For any indicator character next to the message, prioritize deleted, then selected, - // then unread, then attachments. - // First, to ensure the correct status character color is used, copy the menu item colors - // in any of these cases; then change the attributes for the 2nd color (selected). - // Having the separate printf strings for regular, to-user, and from-user are a bit - // bit pointless now that coloring & alternate coloring is done via DDLightbarMenu - if (this.msgReader.MessageIsSelected(this.msgReader.subBoardCode, pItemIndex) || Boolean(msgHdr.attr & MSG_DELETE) || !Boolean(msgHdr.attr & MSG_READ) || Boolean(msgHdr.attr & MSG_REPLIED) || msgHdrHasAttachmentFlag(msgHdr)) - { - menuItemObj.itemColor = []; - var colorSet = this.colors.itemColor; - var selectedColorSet = this.colors.selectedItemColor; - if (!readingPersonalEmail) - { - if (msgIsToUser) - { - colorSet = this.colors.altItemColor; - selectedColorSet = this.colors.altSelectedItemColor; - } - else if (msgIsFromUser) - { - colorSet = [{start: this.msgListIdxes.msgNumStart, end: this.msgListIdxes.msgNumEnd, attrs: this.msgReader.colors.msgListFromUserMsgNumColor}, - {start: this.msgListIdxes.selectMarkStart, end: this.msgListIdxes.selectMarkEnd, attrs: this.msgReader.colors.selectedMsgMarkColor}, - {start: this.msgListIdxes.fromNameStart, end: this.msgListIdxes.fromNameEnd, attrs: this.msgReader.colors.msgListFromUserFromColor}, - {start: this.msgListIdxes.toNameStart, end: this.msgListIdxes.toNameEnd, attrs: this.msgReader.colors.msgListFromUserToColor}, - {start: this.msgListIdxes.subjStart, end: this.msgListIdxes.subjEnd, attrs: this.msgReader.colors.msgListFromUserSubjectColor}, - {start: this.msgListIdxes.dateStart, end: this.msgListIdxes.dateEnd, attrs: this.msgReader.colors.msgListFromUserDateColor}, - {start: this.msgListIdxes.timeStart, end: this.msgListIdxes.timeEnd, attrs: this.msgReader.colors.msgListFromUserTimeColor}]; - selectedColorSet = this.colors.altSelectedItemColor; - } - } - for (var i = 0; i < colorSet.length; ++i) - menuItemObj.itemColor.push(colorSet[i]); - menuItemObj.itemSelectedColor = []; - for (var i = 0; i < selectedColorSet.length; ++i) - menuItemObj.itemSelectedColor.push(selectedColorSet[i]); - } - // Change the color - // Deleted - if (Boolean(msgHdr.attr & MSG_DELETE)) - { - if (menuItemObj.itemColor.length >= 2) - menuItemObj.itemColor[1].attrs = "\x01r\x01h\x01i"; - if (menuItemObj.itemSelectedColor.length >= 2) - menuItemObj.itemSelectedColor[1].attrs = "\x01r\x01h\x01i" + this.msgReader.colors.msgListHighlightBkgColor; - } - // Selected, unread, replied, or has attachments - else if (this.msgReader.MessageIsSelected(this.msgReader.subBoardCode, pItemIndex) || !Boolean(msgHdr.attr & MSG_READ) || Boolean(msgHdr.attr & MSG_REPLIED) || msgHdrHasAttachmentFlag(msgHdr)) - { - if (menuItemObj.itemColor.length >= 2) - menuItemObj.itemColor[1].attrs = "\x01n" + this.msgReader.colors.selectedMsgMarkColor; - if (menuItemObj.itemSelectedColor.length >= 2) - menuItemObj.itemSelectedColor[1].attrs = "\x01n" + this.msgReader.colors.selectedMsgMarkColor + this.msgReader.colors.msgListHighlightBkgColor; - } - } - return menuItemObj; - }; - - // Adjust the menu indexes to ensure they're correct for the current sub-board - this.AdjustLightbarMsgListMenuIdxes(msgListMenu); - - return msgListMenu; -} -// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose -// a message group. -// -// Return value: A DDLightbarMenu object set up to let the user choose a message group -function DigDistMsgReader_CreateLightbarMsgGrpMenu() -{ - // Start & end indexes for the various items in each mssage group list row - // Selected mark, group#, description, # sub-boards - var msgGrpListIdxes = { - markCharStart: 0, - markCharEnd: 1, - grpNumStart: 1, - grpNumEnd: 2 + (+this.areaNumLen) - }; - msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd; - msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen; - msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd; - msgGrpListIdxes.numItemsEnd = msgGrpListIdxes.numItemsStart + +this.numItemsLen; - // Set numItemsEnd to -1 to let the whole rest of the lines be colored - msgGrpListIdxes.numItemsEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 2; - var msgGrpMenuHeight = console.screen_rows - listStartRow; - var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight); - msgGrpMenu.scrollbarEnabled = true; - msgGrpMenu.borderEnabled = false; - msgGrpMenu.SetColors({ - itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor}, - {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor}, - {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor}, - {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor}], - selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor}, - {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor}, - {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor}, - {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor}] - }); - - msgGrpMenu.multiSelect = false; - msgGrpMenu.ampersandHotkeysInItems = false; - msgGrpMenu.wrapNavigation = false; - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - msgGrpMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); - - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - msgGrpMenu.msgReader = this; // Add this object to the menu object - msgGrpMenu.NumItems = function() { - return msg_area.grp_list.length; - }; - msgGrpMenu.GetItem = function(pGrpIndex) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length)) - { - menuItemObj.text = format(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.msgReader.subBoardCode].grp_index)) ? "*" : " "); - menuItemObj.text += format(this.msgReader.msgGrpListPrintfStr, +(pGrpIndex+1), - msg_area.grp_list[pGrpIndex].description.substr(0, this.msgReader.msgGrpDescLen), - msg_area.grp_list[pGrpIndex].sub_list.length); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pGrpIndex; - } - - return menuItemObj; - }; - - // Set the currently selected item to the current group - msgGrpMenu.SetSelectedItemIdx(msg_area.sub[this.subBoardCode].grp_index); - /* - msgGrpMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].grp_index; - if (msgGrpMenu.selectedItemIdx >= msgGrpMenu.topItemIdx+msgGrpMenu.GetNumItemsPerPage()) - msgGrpMenu.topItemIdx = msgGrpMenu.selectedItemIdx - msgGrpMenu.GetNumItemsPerPage() + 1; - */ - - return msgGrpMenu; -} -// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose -// a sub-board within a message group. -// -// Parameters: -// pGrpIdx: The index of the group to list sub-boards for -// -// Return value: A DDLightbarMenu object set up to let the user choose a sub-board within the -// given message group -function DigDistMsgReader_CreateLightbarSubBoardMenu(pGrpIdx) -{ - // Start & end indexes for the various items in each sub-board list row - // Selected mark, group#, description, # sub-boards - var subBrdListIdxes = { - markCharStart: 0, - markCharEnd: 1, - subNumStart: 1, - subNumEnd: 2 + (+this.areaNumLen) - }; - subBrdListIdxes.descStart = subBrdListIdxes.subNumEnd; - subBrdListIdxes.descEnd = subBrdListIdxes.descStart + +(this.subBoardListPrintfInfo[pGrpIdx].nameLen) + 1; - subBrdListIdxes.numItemsStart = subBrdListIdxes.descEnd; - subBrdListIdxes.numItemsEnd = subBrdListIdxes.numItemsStart + +(this.subBoardListPrintfInfo[pGrpIdx].numMsgsLen) + 1; - subBrdListIdxes.dateStart = subBrdListIdxes.numItemsEnd; - subBrdListIdxes.dateEnd = subBrdListIdxes.dateStart + +this.dateLen + 1; - subBrdListIdxes.timeStart = subBrdListIdxes.dateEnd; - // Set timeEnd to -1 to let the whole rest of the lines be colored - subBrdListIdxes.timeEnd = -1; - var listStartRow = this.areaChangeHdrLines.length + 3; - var subBrdMenuHeight = console.screen_rows - listStartRow; - var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBrdMenuHeight); - subBoardMenu.scrollbarEnabled = true; - subBoardMenu.borderEnabled = false; - subBoardMenu.SetColors({ - itemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor}, - {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor}, - {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor}, - {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor}, - {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaLatestDateColor}, - {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaLatestTimeColor}], - selectedItemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor}, - {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor}, - {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor}, - {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor}, - {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaDateHighlightColor}, - {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaTimeHighlightColor}] - }); - - subBoardMenu.multiSelect = false; - subBoardMenu.ampersandHotkeysInItems = false; - subBoardMenu.wrapNavigation = false; - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - subBoardMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F); - - // Add the sub-board items to the menu - for (var subIdx = 0; subIdx < msg_area.grp_list[pGrpIdx].sub_list.length; ++subIdx) - { - var itemText = this.GetMsgSubBoardLine(pGrpIdx, subIdx, false); - subBoardMenu.Add(strip_ctrl(itemText), subIdx); - } - // Alternately, we could change the menu's NumItems() and GetItem(): - /* - // Change the menu's NumItems() and GetItem() function to reference - // the message list in this object rather than add the menu items - // to the menu - subBoardMenu.msgReader = this; // Add this object to the menu object - subBoardMenu.grpIdx = pGrpIdx; - subBoardMenu.NumItems = function() { - return msg_area.grp_list[pGrpIdx].sub_list.length; - }; - subBoardMenu.GetItem = function(pSubIdx) { - var menuItemObj = this.MakeItemWithRetval(-1); - if ((pSubIdx >= 0) && (pSubIdx < msg_area.grp_list[this.grpIdx].sub_list.length)) - { - //var highlight = (msg_area.grp_list[this.grpIdx].sub_list[pSubIdx].code.toUpperCase() == this.msgReader.subBoardCode.toUpperCase()); - menuItemObj.text = this.msgReader.GetMsgSubBoardLine(this.grpIdx, pSubIdx, false); - menuItemObj.text = strip_ctrl(menuItemObj.text); - menuItemObj.retval = pSubIdx; - } - - return menuItemObj; - }; - */ - - // Set the currently selected item to the current group - if (msg_area.sub[this.subBoardCode].grp_index == pGrpIdx) - { - subBoardMenu.SetSelectedItemIdx(msg_area.sub[this.subBoardCode].index); - /* - subBoardMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].index; - if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage()) - subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1; - */ - } - else - { - subBoardMenu.SetSelectedItemIdx(0); - /* - subBoardMenu.selectedItemIdx = 0; - subBoardMenu.topItemIdx = 0; - */ - } - - return subBoardMenu; -} -// For the DigDistMsgLister class: Adjusts lightbar menu indexes for a message list menu -function DigDistMsgReader_AdjustLightbarMsgListMenuIdxes(pMsgListMenu) -{ - pMsgListMenu.SetSelectedItemIdx(this.lightbarListSelectedMsgIdx); - /* - pMsgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx; - pMsgListMenu.topItemIdx = this.lightbarListTopMsgIdx; - */ - - // In the DDLightbarMenu class, the top index on the last page should - // allow for displaying a full page of items. So if - // this.lightbarListTopMsgIdx is beyond the top index for the last - // page in the menu object, then adjust this.lightbarListTopMsgIdx. - var menuTopItemIdxOnLastPage = pMsgListMenu.GetTopItemIdxOfLastPage(); - if (pMsgListMenu.topItemIdx > menuTopItemIdxOnLastPage) - { - pMsgListMenu.topItemIdx = menuTopItemIdxOnLastPage; - this.lightbarListTopMsgIdx = menuTopItemIdxOnLastPage; - } - // TODO: Ensure this.lightbarListTopMsgIdx is always correct for the last page -} -// 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 -// pReturnStrInstead: Optional boolean - Whether or not to return a formatted string -// instead of printing to the console. Defaults to false. -function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pReturnStrInstead) -{ - // 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 msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHeader); - if (msgWrittenLocalTime != -1) - { - sDate = strftime("%Y-%m-%d", msgWrittenLocalTime); - sTime = strftime("%H:%M:%S", msgWrittenLocalTime); - } - 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); - var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : this.GetMsgIdx(pMsgHeader.number)+1); - if (msgNum == 0) // In case GetMsgIdx() returns -1 for failure - msgNum = 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 = " "; - - // Get the message score value - var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHeader); - // Ensure the score number can fit within 4 digits - if (msgVoteInfo.voteScore > 9999) - msgVoteInfo.voteScore = 9999; - else if (msgVoteInfo.voteScore < -999) - msgVoteInfo.voteScore = -999; - - // Generate the string with the message header information. - var msgHdrStr = ""; - // 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) - { - // For any indicator character next to the message, prioritize selected, then deleted, - // then unread, then attachments - if (this.MessageIsSelected(this.subBoardCode, msgNum-1)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.selected + "\x01n"; - else if (msgDeleted) - msgIndicatorChar = "\x01n\x01r\x01h\x01i" + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.deleted + "\x01n"; - else if (this.readingPersonalEmail && !Boolean(pMsgHeader.attr & MSG_READ)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.unread + "\x01n"; - else if (this.readingPersonalEmail && Boolean(pMsgHeader.attr & MSG_REPLIED)) - { - if (this.userSettings.displayMsgRepliedChar) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.replied + "\x01n"; - } - else if (msgHdrHasAttachmentFlag(pMsgHeader)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.attachments + "\x01n"; - var fromName = pMsgHeader.from; - // If the message was posted anonymously and the logged-in user is - // not the sysop, then show "Anonymous" for the 'from' name. - if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - fromName = "Anonymous"; - if (this.showScoresInMsgList) - { - msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar, - fromName.substr(0, this.FROM_LEN), - pMsgHeader.to.substr(0, this.TO_LEN), - pMsgHeader.subject.substr(0, this.SUBJ_LEN), - msgVoteInfo.voteScore, sDate, sTime); - } - else - { - msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar, - fromName.substr(0, this.FROM_LEN), - pMsgHeader.to.substr(0, this.TO_LEN), - pMsgHeader.subject.substr(0, this.SUBJ_LEN), - sDate, sTime); - } - } - else - { - // For any indicator character next to the message, prioritize selected, then deleted, - // then unread, then attachments - if (this.MessageIsSelected(this.subBoardCode, msgNum-1)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.selected + "\x01n"; - else if (msgDeleted) - msgIndicatorChar = "\x01n\x01r\x01h\x01i" + this.msgListStatusChars.deleted + "\x01n"; - else if (this.readingPersonalEmail && !Boolean(pMsgHeader.attr & MSG_READ)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.unread + "\x01n"; - else if (this.readingPersonalEmail && Boolean(pMsgHeader.attr & MSG_REPLIED)) - { - if (this.userSettings.displayMsgRepliedChar) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.replied + "\x01n"; - } - else if (msgHdrHasAttachmentFlag(pMsgHeader)) - msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.attachments + "\x01n"; - - // 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 msgToUser = userHandleAliasNameMatch(pMsgHeader.to); - var fromNameUpper = pMsgHeader.from.toUpperCase(); - var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase())); - var formatStr = ""; // Format string for printing the message information - if (this.readingPersonalEmail) - formatStr = this.sMsgInfoFormatStr; - else - formatStr = (msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr)); - var fromName = pMsgHeader.from; - // If the message was posted anonymously and the logged-in user is - // not the sysop, then show "Anonymous" for the 'from' name. - if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - fromName = "Anonymous"; - if (this.showScoresInMsgList) - { - msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN), - pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN), - msgVoteInfo.voteScore, sDate, sTime); - } - else - { - msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN), - pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN), - sDate, sTime); - } - } - - var returnStrInstead = (typeof(pReturnStrInstead) == "boolean" ? pReturnStrInstead : false); - if (!returnStrInstead) - { - console.print(msgHdrStr); - console.cleartoeol("\x01n"); // To clear away any extra text that may have been entered by the user - } - return msgHdrStr; -} -// 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) -// 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, pAllowChgSubBoard) -{ - // Create the return object and set some initial default values - var retObj = { - continueOn: true, - userInput: "", - 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 - allowedKeys += this.msgListKeys.userSettings; - if (allowChgSubBoard) - allowedKeys += "C"; // Change to another message area - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - allowedKeys += "D"; // Delete - allowedKeys += this.enhReaderKeys.deleteMessage; - allowedKeys += '\x7f'; - allowedKeys += '\x08'; - } - 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 != this.enhReaderKeys.batchDelete) // 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)) - { - // See if the current message header has our "isBogus" property and it's true. - // Only let the user read the message if it's not a bogus message header. - // The message header could have the "isBogus" property, for instance, if - // it's a vote message (introduced in Synchronet 3.17). - var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false); - if (tmpMsgHdr != null) - { - // 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); - // Return from here so that the calling function can switch - // into reader mode. - retObj.continueOn = continueOn; - retObj.userInput = userInput; - retObj.selectedMsgOffset = userInput-1; - return retObj; - } - 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 - { - console.print("\x01n\x01h\x01yThat's not a readable message.\x01n"); - console.crlf(); - console.pause(); - } - } - else - { - // The user entered an invalid message number. - console.print("\x01n\x01h\x01w" + userInput + " \x01y is not a valid message number.\x01n"); - console.crlf(); - console.pause(); - continueOn = true; - } - } - - // Make sure color highlighting is turned off - console.attributes = "N"; - - // 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, lets the user read a message and allows the user to -// respond, etc. 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 = { - offsetValid: true, - msgNotReadable: false, - userReplied: false, - lastKeypress: "", - newMsgOffset: -1, - nextAction: ACTION_NONE, - refreshEnhancedRdrHelpLine: false - }; - - // Get the message header. Don't expand fields since we may need to save - // the header later with the MSG_READ attribute. - //var msgHeader = this.GetMsgHdrByIdx(pOffset, false); - // Get the message header. Get expanded fields so that we can show any - // voting stats/responses that may be included with the message. - var msgHeader = this.GetMsgHdrByIdx(pOffset, true); - if (msgHeader == null) - { - //console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, +(pOffset+1))) + "\x01n"); - //console.crlf(); - console.putmsg("\x01n" + format(this.text.invalidMsgNumText, +(pOffset+1)) + "\x01n"); - console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS); - retObj.offsetValid = false; - return retObj; - } - - // If this message is not readable for the user (it's marked as deleted and - // the system is set to not show deleted messages, etc.), then don't let the - // user read it, and just silently return. - // TODO: If the message is not readable, this will end up causing an infinite loop. - retObj.msgNotReadable = !isReadableMsgHdr(msgHeader, this.subBoardCode); - if (retObj.msgNotReadable) - return retObj; - - // Mark the message as read if reading personal email or if it was written to the current user - if (this.readingPersonalEmail || ((msgHeader.attr & MSG_READ) == 0) && (userHandleAliasNameMatch(msgHeader.to))) - { - // Using applyAttrsInMsgHdrInMessagbase(), which loads the header without - // expanded fields and saves the attributes with that header. - var saveRetObj = applyAttrsInMsgHdrInMessagbase(this.subBoardCode, msgHeader.number, MSG_READ); - if (this.SearchTypePopulatesSearchResults() && saveRetObj.saveSucceeded) - this.RefreshHdrInSavedArrays(pOffset, MSG_READ, true); - } - // For personal email, if we wanted to really check that it was written to the current sysop user: - //var personalEmailToCurrentSysopUser = this.readingPersonalEmail && user.is_sysop && msgHeader.to.toUpperCase().indexOf("SYSOP") == 0; - //if (((msgHeader.attr & MSG_READ) == 0) && (userHandleAliasNameMatch(msgHeader.to) || personalEmailToCurrentSysopUser)) - - // Updating message pointers etc. - updateScanPtrAndOrLastRead(this.subBoardCode, msgHeader, this.doingMsgScan); - - // 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"); - - // Get the message text and see if it has any ANSI codes. Remove any pause - // codes it might have. If it has ANSI codes, then don't use the scrolling - // interface so that the ANSI gets displayed properly. - var getMsgBodyRetObj = this.GetMsgBody(msgHeader); - var messageText = getMsgBodyRetObj.msgBody; - - if (msgHdrHasAttachmentFlag(msgHeader)) - { - messageText = "\x01n\x01g\x01h- This message contains one or more attachments. Press CTRL-A to download.\x01n\r\n" - + "\x01n\x01g\x01h--------------------------------------------------------------------------\x01n\r\n" - + messageText; - } - var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI); - // 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); - // If the current message is new to the user, update the number of posts read this session. - if (pOffset > this.GetScanPtrMsgIdx()) - ++bbs.posts_read; - // 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) - retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode); - else - retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode); - - return retObj; -} -// Helper method for ReadMessageEnhanced() - Does the scrollable reader interface -function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, pmode) -{ - var retObj = { - offsetValid: true, - msgNotReadable: false, - userReplied: false, - lastKeypress: "", - newMsgOffset: -1, - nextAction: ACTION_NONE, - refreshEnhancedRdrHelpLine: false - }; - - // This is a scrollbar update function for use when viewing the message. - 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.UpdateEnhancedReaderScrollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks); - solidBlockLastStartRow = solidBlockStartRow; - console.gotoxy(1, console.screen_rows); - } - // This is a scrollbar update function for use when viewing the header info/kludge lines. - function msgInfoScrollbarUpdateFn(pFractionToLastPage) - { - var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage); - if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow) - msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks); - lastInfoSolidBlockStartRow = infoSolidBlockStartRow; - console.gotoxy(1, console.screen_rows); - } - - var msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1; - - // We could word-wrap the message to ensure words aren't split across lines, but - // doing so could make some messages look bad (i.e., messages with drawing characters), - // and word_wrap also might not handle ANSI or other color/attribute codes.. - //if (!textHasDrawingChars(messageText)) - // messageText = word_wrap(messageText, msgAreaWidth); - - // If the message has ANSI content, then use a Graphic object to help make - // the message look good. Also, remove any ANSI clear screen codes from the - // message text. - var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0; - if (msgHasANSICodes) - { - messageText = messageText.replace(/\u001b\[[012]J/gi, ""); - //var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1); - // To help ensure ANSI messages look good, it seems the Graphic object should have - // its with later set to 1 less than the width used to create it. - var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns); - var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1); - graphic.auto_extend = true; - graphic.ANSI = ansiterm.expand_ctrl_a(messageText); - //graphic.normalize(); - graphic.width = graphicWidth - 1; - //messageText = graphic.MSG.split('\n'); - messageText = graphic.MSG; - } - - // 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); - - var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage; - 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; - - // If use of the scrollbar is enabled, 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. - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - - // Input loop (for scrolling the message up & down) - var msgLineFormatStr = "%-" + msgAreaWidth + "s"; - var writeMessage = true; - // msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use - // with scrollTextLines(). - var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1; - var msgReaderObj = this; - - var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader); - - // For editing a user account. Only for the sysop. - function userEdit(msgHeader, pOffset, pReader) - { - console.attributes = "N"; - console.crlf(); - console.print("- Edit user " + msgHeader.from); - console.crlf(); - var editObj = editUser(msgHeader.from); - if (editObj.errorMsg.length != 0) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01y\x01h" + editObj.errorMsg + "\x01n"); - console.crlf(); - console.pause(); - } - } - - // For viewing the hex dump information (for the sysop) - var msgHexInfo = null; - - // 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 scrollbarInfoObj = { - solidBlockLastStartRow: 0, - numSolidScrollBlocks: 0 - }; - scrollbarInfoObj.solidBlockLastStartRow = solidBlockLastStartRow; - scrollbarInfoObj.numSolidScrollBlocks = numSolidScrollBlocks; - var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx, - this.colors.msgBodyColor, writeMessage, - this.msgAreaLeft, this.msgAreaTop, msgAreaWidth, - msgAreaHeight, 1, console.screen_rows, - this.userSettings.useEnhReaderScrollbar, - msgScrollbarUpdateFn, pmode); - topMsgLineIdx = scrollRetObj.topLineIdx; - retObj.lastKeypress = scrollRetObj.lastKeypress; - switch (retObj.lastKeypress) - { - case this.enhReaderKeys.deleteMessage: // Delete message - case '\x7f': - case '\x08': - 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.PromptAndDeleteOrUndeleteMessage() will check to see if the user - // is a sysop or the message was posted by the user. - // If the message was deleted and the user can't view deleted messages, - // 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.PromptAndDeleteOrUndeleteMessage(pOffset, promptPos, true, true, msgAreaWidth, true); - if (msgWasDeleted && !canViewDeletedMsgs()) - { - var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); - continueOn = msgSearchObj.continueInputLoop; - retObj.newMsgOffset = msgSearchObj.newMsgOffset; - retObj.nextAction = msgSearchObj.nextAction; - if (msgSearchObj.promptGoToNextArea) - { - if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(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 this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.) - var originalCurpos = console.getxy(); - var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); - var selected = this.EnhReaderPromptYesNo("Select this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true); - this.ToggleSelectedMessage(this.subBoardCode, pOffset, selected); - writeMessage = false; // No need to refresh the message - break; - case this.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.DeleteOrUndeleteSelectedMessages() 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 this.enhReaderKeys.editMsg: // 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 = "\x01n\x01h\x01k" + BLOCK1; // Dim block - // Dim block - if (this.userSettings.useEnhReaderScrollbar) - { - var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar; - if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom) - { - //scrollBarBlock = "\x01w" + BLOCK2; // Bright block - // Bright block - scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar; - } - else - { - // TODO - } - 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("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - } - } - } - else - writeMessage = false; // Don't write the current message again - break; - case this.enhReaderKeys.showHelp: // Show the help screen - this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments); - // 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("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - break; - case this.enhReaderKeys.reply: // Reply to the message - case this.enhReaderKeys.privateReply: // Private message reply - // If the user pressed the private reply key while reading private - // mail, then do nothing (allow only the regular reply key to reply). - var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply); - if (privateReply && this.readingPersonalEmail) - writeMessage = false; // Don't re-write the current message again - else - { - // Get the message header with fields expanded so we can get the most info possible. - //var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true); - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true); - msgbase.close(); - // Let the user reply to the message. - var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset); - retObj.userReplied = replyRetObj.postSucceeded; - //retObj.msgNotReadable = replyRetObj.msgWasDeleted; - var msgWasDeleted = replyRetObj.msgWasDeleted; - //if (retObj.msgNotReadable) - if (msgWasDeleted && !canViewDeletedMsgs()) - { - var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); - continueOn = msgSearchObj.continueInputLoop; - retObj.newMsgOffset = msgSearchObj.newMsgOffset; - retObj.nextAction = msgSearchObj.nextAction; - if (msgSearchObj.promptGoToNextArea) - { - if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(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 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("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - } - } - } - break; - case this.enhReaderKeys.postMsg: // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - 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, replaceAtCodesInStr(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, - "\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n", - ERROR_PAUSE_WAIT_MS, "\x01n", 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.attributes = "N"; - 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 this.enhReaderKeys.prevMsgByTitle: // Previous message by title - case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author - case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user' - case this.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, this.enhReaderKeys), - 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 this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject) - case this.enhReaderKeys.nextMsgByAuthor: // Next message by author - case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user' - case this.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, this.enhReaderKeys), - 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 this.enhReaderKeys.previousMsg: // 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.FindNextReadableMsgIdx(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(replaceAtCodesInStr(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 this.enhReaderKeys.nextMsg: // 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. - var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks); - if (findNextMsgRetObj.newMsgOffset > -1) - retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset; - writeMessage = findNextMsgRetObj.writeMessage; - continueOn = findNextMsgRetObj.continueOn; - retObj.nextAction = findNextMsgRetObj.nextAction; - break; - // First & last message: Quit out of this input loop and let the - // calling function, this.ReadMessages(), handle the action. - case this.enhReaderKeys.firstMsg: // 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 this.enhReaderKeys.lastMsg: // 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 this.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 this.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 this.enhReaderKeys.showHdrInfo: - case this.enhReaderKeys.showKludgeLines: - if (user.is_sysop) - { - writeMessage = this.ShowHdrOrKludgeLines_Scrollable(retObj.lastKeypress == this.enhReaderKeys.showKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight); - // Display the scrollbar for the message to refresh it on the screen - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - } - 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 this.enhReaderKeys.showMsgList: // Message list - console.attributes = "N"; - console.crlf(); - console.print("Loading..."); - retObj.nextAction = ACTION_DISPLAY_MSG_LIST; - continueOn = false; - break; - case this.enhReaderKeys.chgMsgArea: // 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 this.enhReaderKeys.downloadAttachments: // Download attachments - if (msgHasAttachments) - { - console.attributes = "N"; - console.gotoxy(1, console.screen_rows); - console.crlf(); - allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode); - - // Refresh things on the screen - console.clear("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - } - else - writeMessage = false; - break; - case this.enhReaderKeys.saveToBBSMachine: - // Save the message to the BBS machine - Only allow this - // if the user is a sysop. - if (user.is_sysop) - { - // Prompt the user for a filename to save the message to the - // BBS machine - var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); - console.print("\x01n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - if (filename.length > 0) - { - var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, promptPos); - console.gotoxy(promptPos); - console.cleartoeol("\x01n"); - console.gotoxy(promptPos); - if (saveMsgRetObj.succeeded) - { - var statusMsg = "\x01n\x01cThe message has been saved."; - if (msgHdrHasAttachmentFlag(msgHeader)) - statusMsg += " Attachments not saved."; - statusMsg += "\x01n"; - console.print(statusMsg); - } - else - console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - else - { - console.gotoxy(promptPos); - console.print("\x01n\x01y\x01hMessage not exported\x01n"); - 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 - break; - case this.enhReaderKeys.userEdit: // Edit the user who wrote the message - if (user.is_sysop) - { - userEdit(msgHeader, pOffset, this); - // Refresh things on the screen - console.clear("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - retObj.solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - } - else // The user is not a sysop - writeMessage = false; - break; - case this.enhReaderKeys.forwardMsg: // Forward the message - console.attributes = "N"; - console.crlf(); - console.print("\x01c- Forward message\x01n"); - console.crlf(); - var retStr = this.ForwardMessage(msgHeader, messageText); - if (retStr.length > 0) - { - console.print("\x01n\x01h\x01y* " + retStr + "\x01n"); - console.crlf(); - console.pause(); - } - - // Refresh things on the screen - console.clear("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - break; - case this.enhReaderKeys.vote: // Vote on the message - // Move the cursor to the last line in the message area so the - // vote question text prompt will appear there. - var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); - console.gotoxy(1, console.screen_rows-1); - // Let the user vote on the message - var voteRetObj = this.VoteOnMessage(msgHeader, true); - if (voteRetObj.BBSHasVoteFunction) - { - var msgIsPollVote = ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL); - if (!voteRetObj.userQuit) - { - // If the message is a poll vote, then output any error - // message on its own line and refresh the whole screen. - // Otherwise, use the last 2 rows for an error message - // and only refresh what's necessary. - if (msgIsPollVote) - { - console.attributes = "N"; - console.gotoxy(1, console.screen_rows-1); - if (voteRetObj.errorMsg.length > 0) - { - if (voteRetObj.mnemonicsRequiredForErrorMsg) - { - console.mnemonics(voteRetObj.errorMsg); - console.attributes = "N"; - } - else - console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - else if (!voteRetObj.savedVote) - { - console.print("\x01y\x01h* Failed to save the vote\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - } - else - { - // Not a poll vote - Just an up/down vote - if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote)) - { - console.attributes = "N"; - console.gotoxy(1, console.screen_rows-1); - if (voteRetObj.errorMsg.length > 0) - { - if (voteRetObj.mnemonicsRequiredForErrorMsg) - { - console.mnemonics(voteRetObj.errorMsg); - console.attributes = "N"; - } - else - console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n"); - } - else if (!voteRetObj.savedVote) - console.print("\x01y\x01h* Failed to save the vote\x01n"); - } - else - msgHeader = voteRetObj.updatedHdr; // To get updated vote information - mswait(ERROR_PAUSE_WAIT_MS); - - writeMessage = false; - } - // Exit out of the reader and come back to read - // the same message again so that the voting results - // are re-loaded and displayed on the screen. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - { - // The user quit out of voting. Refresh the screen. - // Exit out of the reader and come back to read - // the same message again so that the screen is refreshed. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - } - else - writeMessage = false; - break; - case this.enhReaderKeys.showVotes: // Show votes - var DVRetObj = this.ShowVoteInfo_Scrollable(msgHeader, msgAreaWidth, msgAreaHeight); - writeMessage = DVRetObj.writeMessage; - if (DVRetObj.hasVoteProps) - { - // Display the scrollbar for the message to refresh it on the screen - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - } - break; - case this.enhReaderKeys.closePoll: // Close a poll message - // Save the original cursor position - var originalCurPos = console.getxy(); - var pollCloseMsg = ""; - // If this message is a poll, then allow closing it. - if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL) - { - if ((msgHeader.auxattr & POLL_CLOSED) == 0) - { - // Only let the user close the poll if they created it - if (userHandleAliasNameMatch(msgHeader.from)) - { - // Prompt to confirm whether the user wants to close the poll - console.gotoxy(1, console.screen_rows-1); - printf("\x01n%" + +(console.screen_columns-1) + "s", ""); - console.gotoxy(1, console.screen_rows-1); - if (!console.noyes("Close poll")) - { - // Close the poll (open the sub-board first) - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if (closePollWithOpenMsgbase(msgbase, msgHeader.number)) - { - msgHeader.auxattr |= POLL_CLOSED; - pollCloseMsg = "\x01n\x01cThis poll was successfully closed."; - } - else - pollCloseMsg = "\x01n\x01r\x01h* Failed to close this poll!"; - msgbase.close(); - } - else - pollCloseMsg = "\x01n\x01y\x01hUnable to open sub-board to close the poll"; - } - } - else - pollCloseMsg = "\x01n\x01y\x01hCan't close this poll because it's not yours"; - } - else - pollCloseMsg = "\x01n\x01y\x01hThis poll is already closed"; - } - else - pollCloseMsg = "This message is not a poll"; - - // Display the poll closing status message - this.DisplayEnhReaderError(pollCloseMsg, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - console.gotoxy(originalCurPos); - writeMessage = false; - break; - case this.enhReaderKeys.validateMsg: // Validate the message - if (user.is_sysop && (this.subBoardCode != "mail") && msg_area.sub[this.subBoardCode].is_moderated) - { - var message = ""; - if (this.ValidateMsg(this.subBoardCode, msgHeader.number)) - { - message = "\x01n\x01cMessage validation successful"; - // Refresh the message header in the arrays - this.RefreshMsgHdrInArrays(msgHeader.number); - // Exit out of the reader and come back to read - // the same message again so that the voting results - // are re-loaded and displayed on the screen. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - message = "\x01n\x01y\x01hMessage validation failed!"; - this.DisplayEnhReaderError(message, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - } - else - writeMessage = false; - break; - case this.enhReaderKeys.quickValUser: // Quick-validate the user - if (user.is_sysop) - { - var valRetObj = quickValidateLocalUser(msgHeader.from, this.scrollingReaderInterface && console.term_supports(USER_ANSI), this.quickUserValSetIndex); - if (valRetObj.needWholeScreenRefresh) - { - this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - else - { - // TODO - } - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - { - writeMessage = false; // Don't refresh the whole message - if (valRetObj.refreshBottomLine) - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - if (valRetObj.optionBoxTopLeftX > 0 && valRetObj.optionBoxTopLeftY > 0 && valRetObj.optionBoxWidth > 0 && valRetObj.optionBoxHeight > 0) - this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, valRetObj.optionBoxTopLeftX, valRetObj.optionBoxTopLeftY, valRetObj.optionBoxWidth, valRetObj.optionBoxHeight); - } - } - else - writeMessage = false; - break; - case this.enhReaderKeys.bypassSubBoardInNewScan: - writeMessage = false; // TODO: Finish - /* - if (this.doingMsgScan) - { - console.attributes = "N"; - 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(); - if (!console.noyes("Bypass this sub-board in newscans")) - { - continueOn = false; - msg_area.sub[this.subBoardCode].scan_cfg &= SCAN_CFG_NEW; - } - else - { - this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - // Move the cursor back to its original position - console.gotoxy(originalCurpos); - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - } - else - writeMessage = false; - */ - break; - case this.enhReaderKeys.userSettings: - // Make a backup copy of the this.userSettings.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar - var oldUseEnhReaderScrollbar = this.userSettings.useEnhReaderScrollbar; - var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { pReader.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); }); - retObj.lastKeypress = ""; - writeMessage = userSettingsRetObj.needWholeScreenRefresh; - // In case the user changed their twitlist, re-filter the messages for this sub-board - if (userSettingsRetObj.userTwitListChanged) - { - console.gotoxy(1, console.screen_rows); - console.crlf(); - console.print("\x01nTwitlist changed; re-filtering.."); - var tmpMsgbase = new MsgBase(this.subBoardCode); - if (tmpMsgbase.open()) - { - continueOn = false; - writeMessage = false; - var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true); - tmpMsgbase.close(); - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true); - // If the user is currently reading a message a message by someone who is now - // in their twit list, change the message currently being viewed. - if (this.MsgHdrFromOrToInUserTwitlist(msgHeader)) - { - var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks); - if (findNextMsgRetObj.newMsgOffset > -1) - { - retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - } - else - retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; - } - else - { - // If there are still messages in this sub-board, and the message offset is beyond the last - // message, then show the last message in the sub-board. Otherwise, go to the next message area. - if (this.hdrsForCurrentSubBoard.length > 0) - { - if (pOffset > this.hdrsForCurrentSubBoard.length) - { - //this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number - retObj.newMsgOffset = this.hdrsForCurrentSubBoard.length - 1; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - } - } - else - retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; - } - } - else - console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p"); - this.SetUpLightbarMsgListVars(); - writeMessage = true; - } - if (userSettingsRetObj.needWholeScreenRefresh) - { - this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - else - { - // TODO - } - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - { - // If the message scrollbar was toggled, then draw/erase the scrollbar - if (this.userSettings.useEnhReaderScrollbar != oldUseEnhReaderScrollbar) - { - msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1; - // If the message is ANSI, then re-create the Graphic object to account for the - // new width - if (msgHasANSICodes) - { - //var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1); - // To help ensure ANSI messages look good, it seems the Graphic object should have - // its with later set to 1 less than the width used to create it. - var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns); - var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1); - graphic.auto_extend = true; - graphic.ANSI = ansiterm.expand_ctrl_a(messageText); - //graphic.width = msgAreaWidth; - graphic.width = graphicWidth - 1; - messageText = graphic.MSG; - } - // Display or erase the scrollbar - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // Erase the scrollbar - console.attributes = "N"; - for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(" "); - } - } - } - this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight); - } - break; - case this.enhReaderKeys.showMsgHex: - if (user.is_sysop) - { - writeMessage = false; - if (msgHexInfo == null) - msgHexInfo = this.GetMsgHexInfo(messageText, true); - if (msgHexInfo.msgHexArray.length > 0) - { - this.ShowMsgHex_Scrolling(msgHexInfo); - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - writeMessage = true; - } - } - else // The user is not a sysop - writeMessage = false; - break; - case this.enhReaderKeys.hexDump: - // Save a hex dump of the message to the BBS machine - Only allow this - // if the user is a sysop. - if (user.is_sysop) - { - // Prompt the user for a filename to save the hex dump to the - // BBS machine - var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); - console.print("\x01n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - if (filename.length > 0) - { - console.gotoxy(promptPos); - console.cleartoeol("\x01n"); - console.gotoxy(promptPos); - var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename); - if (saveHexRetObj.saveSucceeded) - console.print("\x01n\x01cThe hex dump has been saved.\x01n"); - else if (saveHexRetObj.errorMsg != "") - console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n"); - else - console.print("\x01n\x01y\x01hFailed!\x01n"); - } - else - { - console.gotoxy(promptPos); - console.print("\x01n\x01y\x01hHex dump not exported\x01n"); - } - 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 - break; - case this.enhReaderKeys.operatorMenu: // Operator menu - writeMessage = false; - if (user.is_sysop) - { - var opRetObj = this.ShowReadModeOpMenuAndGetSelection(); - // Refresh the message area where the option menu was - this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, opRetObj.menuTopLeftX, opRetObj.menuTopLeftY, opRetObj.menuWidth, opRetObj.menuHeight); - if (opRetObj.chosenOption != null) - { - switch (opRetObj.chosenOption) - { - case this.readerOpMenuOptValues.validateMsg: // Validate the message - if (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated) - { - var message = ""; - if (this.ValidateMsg(this.subBoardCode, msgHeader.number)) - { - message = "\x01n\x01cMessage validation successful"; - // Refresh the message header in the arrays - this.RefreshMsgHdrInArrays(msgHeader.number); - // Exit out of the reader and come back to read - // the same message again so that the voting results - // are re-loaded and displayed on the screen. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - message = "\x01n\x01y\x01hMessage validation failed!"; - this.DisplayEnhReaderError(message, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - writeMessage = true; - } - break; - case this.readerOpMenuOptValues.editAuthorUserAccount: // Edit the local user account - console.gotoxy(1, console.screen_rows); - userEdit(msgHeader, pOffset, this); - // Refresh things on the screen - console.clear("\x01n"); - // 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 - if (this.userSettings.useEnhReaderScrollbar) - { - retObj.solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - writeMessage = true; // We want to refresh the message on the screen - break; - case this.readerOpMenuOptValues.showHdrLines: // Show header or kludge lines - case this.readerOpMenuOptValues.showKludgeLines: - writeMessage = this.ShowHdrOrKludgeLines_Scrollable(opRetObj.chosenOption == this.readerOpMenuOptValues.showKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight); - // Display the scrollbar for the message to refresh it on the screen - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - break; - case this.readerOpMenuOptValues.showTallyStats: // Show tally/vote stats/info - var DVRetObj = this.ShowVoteInfo_Scrollable(msgHeader, msgAreaWidth, msgAreaHeight); - writeMessage = DVRetObj.writeMessage; - if (DVRetObj.hasVoteProps) - { - // Display the scrollbar for the message to refresh it on the screen - if (this.userSettings.useEnhReaderScrollbar) - { - solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage); - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - } - else - { - // TODO - } - } - break; - case this.readerOpMenuOptValues.showMsgHex: - writeMessage = false; - if (msgHexInfo == null) - msgHexInfo = this.GetMsgHexInfo(messageText, true); - if (msgHexInfo.msgHexArray.length > 0) - { - this.ShowMsgHex_Scrolling(msgHexInfo); - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - writeMessage = true; - } - break; - case this.readerOpMenuOptValues.saveMsgHexToFile: - // Prompt the user for a filename to save the hex dump to the - // BBS machine - var promptPos = this.EnhReaderPrepLast2LinesForPrompt(); - console.print("\x01n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - if (filename.length > 0) - { - console.gotoxy(promptPos); - console.cleartoeol("\x01n"); - console.gotoxy(promptPos); - var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename); - if (saveHexRetObj.saveSucceeded) - console.print("\x01n\x01cThe hex dump has been saved.\x01n"); - else if (saveHexRetObj.errorMsg != "") - console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n"); - else - console.print("\x01n\x01y\x01hFailed!\x01n"); - } - else - { - console.gotoxy(promptPos); - console.print("\x01n\x01y\x01hHex dump not exported\x01n"); - } - 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 = true; - break; - case this.readerOpMenuOptValues.quickValUser: // Quick validate the user - var valRetObj = quickValidateLocalUser(msgHeader.from, this.scrollingReaderInterface && console.term_supports(USER_ANSI), this.quickUserValSetIndex); - if (valRetObj.needWholeScreenRefresh) - { - this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks); - else - { - // TODO - } - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - else - { - writeMessage = false; // Don't refresh the whole message - if (valRetObj.refreshBottomLine) - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - if (valRetObj.optionBoxTopLeftX > 0 && valRetObj.optionBoxTopLeftY > 0 && valRetObj.optionBoxWidth > 0 && valRetObj.optionBoxHeight > 0) - this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, valRetObj.optionBoxTopLeftX, valRetObj.optionBoxTopLeftY, valRetObj.optionBoxWidth, valRetObj.optionBoxHeight); - } - break; - case this.readerOpMenuOptValues.addAuthorToTwitList: // Add author to twit list - var promptTxt = format("Add %s to twit list", msgHeader.from); - if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true)) - { - var statusMsg = "\x01n" + addToTwitList(msgHeader.from) ? "\x01w\x01hSuccessfully updated the twit list" : "\x01y\x01hFailed to update the twit list!" - writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true); - console.attributes = "N"; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - break; - case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter: - var fromEmailAddr = ""; - if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0) - { - if (msgHeader.from_net_type == NET_INTERNET) - fromEmailAddr = msgHeader.from_net_addr; - else - fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr; - } - var promptTxt = format("Add %s to global email filter", fromEmailAddr); - if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true)) - { - var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!" - writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true); - console.attributes = "N"; - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); - } - break; - } - } - } - break; - case this.enhReaderKeys.quit: // Quit - case KEY_ESC: - // Normally, if quitFromReaderGoesToMsgList is enabled, then do that - if (this.userSettings.quitFromReaderGoesToMsgList) - { - console.attributes = "N"; - console.crlf(); - console.print("Loading..."); - retObj.nextAction = ACTION_DISPLAY_MSG_LIST; - } - else - retObj.nextAction = ACTION_QUIT; - continueOn = false; - break; - default: - writeMessage = false; - break; - } - } - - return retObj; -} -// Helper method for ReadMessageEnhanced_Scrollable(): Shows header or kludge lines for the scrollable interface. For the sysop. -function DigDistMsgReader_ShowHdrOrKludgeLines_Scrollable(pOnlyKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight) -{ - var msgReaderObj = this; - // This is a scrollbar update function for use when viewing the header info/kludge lines. - function msgInfoScrollbarUpdateFn(pFractionToLastPage) - { - var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage); - if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow) - msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks); - lastInfoSolidBlockStartRow = infoSolidBlockStartRow; - console.gotoxy(1, console.screen_rows); - } - - var writeMessage = false; - - // 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(this.subBoardCode, msgHeader.number, pOnlyKludgeLines); - if (extdHdrInfoLines.length > 0) - { - if (this.userSettings.useEnhReaderScrollbar) - { - // 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; - // 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, msgAreaWidth, msgAreaHeight, 1, console.screen_rows, - this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn); - writeMessage = true; // We want to refresh the message on the screen - } - else - { - // There are no header/kludge lines for this message - var msgText = pOnlyKludgeLines ? this.text.noKludgeLinesForThisMsgText : this.text.noHdrLinesForThisMsgText; - this.DisplayEnhReaderError(replaceAtCodesInStr(msgText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - console.gotoxy(originalCurPos); - } - - return writeMessage; -} -// For the DDMsgReader class: Shows vote/tally information, for the scrollable interface. -function DigDistMsgReader_ShowVoteInfo_Scrollable(pMsgHeader, pMsgAreaWidth, pMsgAreaHeight) -{ - var msgReaderObj = this; - // This is a scrollbar update function for use when viewing the header info/kludge lines. - function msgInfoScrollbarUpdateFn(pFractionToLastPage) - { - var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage); - if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow) - msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks); - lastInfoSolidBlockStartRow = infoSolidBlockStartRow; - console.gotoxy(1, console.screen_rows); - } - - var retObj = { - hasVoteProps: pMsgHeader.hasOwnProperty("total_votes") && pMsgHeader.hasOwnProperty("upvotes"), - writeMessage: false - }; - - // Save the original cursor position - var originalCurPos = console.getxy(); - if (retObj.hasVoteProps) - { - var voteInfo = this.GetUpvoteAndDownvoteInfo(pMsgHeader); - // Display the vote info and let the user scroll through them - // (the console height should be enough, but do this just in case) - // Calculate information for the scrollbar for the vote info lines - if (this.userSettings.useEnhReaderScrollbar) - { - var infoFractionShown = this.pMsgAreaHeight / voteInfo.length; - if (infoFractionShown > 1) - infoFractionShown = 1.0; - var numInfoSolidScrollBlocks = Math.floor(this.pMsgAreaHeight * infoFractionShown); - if (numInfoSolidScrollBlocks == 0) - numInfoSolidScrollBlocks = 1; - var numNonSolidInfoScrollBlocks = this.pMsgAreaHeight - numInfoSolidScrollBlocks; - var lastInfoSolidBlockStartRow = this.msgAreaTop; - // Display the vote info lines and let the user scroll through them - this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks); - } - else - { - // TODO - } - scrollTextLines(voteInfo, 0, this.colors.msgBodyColor, true, this.msgAreaLeft, this.msgAreaTop, pMsgAreaWidth, - pMsgAreaHeight, 1, console.screen_rows, this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn); - retObj.writeMessage = true; // We want to refresh the message on the screen - } - else - { - this.DisplayEnhReaderError("There is no voting information for this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr); - console.gotoxy(originalCurPos); - } - return retObj; -} -// Helper method for ReadMessageEnhanced_Scrollable(): Determines if there is a readable message after the -// one at the given offset. -// -// Parameters: -// pOfset: The offset of the current message -// pMsgInfo: An object contaiining message information -// pTopMsgLineIdx: The index of the message line at the top of the reader area -// pMsgLineFormatStr: A format string for the message line -// pSolidBlockStartRow: The screen row of where the solid blocks start for the scrollbar -// pNumSolidScrollBlocks: The number of solid blocks in the scrollbar -// -// Return value: An object containing the following properties: -// newMsgOffset: The offset of the next readable message, if available. If not available, this will be -1. -// writeMessage: Boolean - Whether or not to write the whole message again (for the scrollable interface) -// continueOn: Boolean - Whether or not to continue with the reader input loop -// nextAction: A value indicating the next action for the reader to take after leaving the reader function -function DigDistMsgReader_ScrollableReaderNextReadableMessage(pOffset, pMsgInfo, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks) -{ - var retObj = { - newMsgOffset: -1, - writeMessage: true, - continueOn: true, - nextAction: ACTION_NONE - }; - - // Look for the next readable message. 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.FindNextReadableMsgIdx(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) - retObj.writeMessage = false; - else - { - // If configured to allow the user to post in the sub-board - // instead of going to the next message area and we're not - // scanning, then do so. - if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan) - { - console.attributes = "N"; - console.crlf(); - // Ask the user if they want to post on the sub-board. - // If they say yes, then do so before exiting. - var grpNameAndDesc = this.GetGroupNameAndDesc(); - if (!console.noyes(replaceAtCodesInStr(format(this.text.postOnSubBoard, grpNameAndDesc.grpName, grpNameAndDesc.grpDesc)))) - bbs.post_msg(this.subBoardCode); - retObj.continueOn = false; - retObj.nextAction = ACTION_QUIT; - } - else - { - // Prompt the user whether they want to go to the next message area - if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), pMsgInfo.messageLines, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks)) - { - // Let this method exit and let the caller go to the next sub-board - retObj.continueOn = false; - retObj.nextAction = ACTION_GO_NEXT_MSG; - } - else - retObj.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. - retObj.continueOn = false; - retObj.nextAction = ACTION_GO_NEXT_MSG; - } - - return retObj; -} -// Helper method for ReadMessageEnhanced() - Determines the next keypress for a click -// coordinate outside the scroll area. -// -// Parameters: -// pScrollRetObj: The return object of the message scroll function -// pEnhReadHelpLineClickCoords: An array of click coordinates & action strings -// -// Return value: An object containing the following properties: -// actionStr: A string containing the next action for the enhanced reader, -// or an empty string if there was no valid action found. -function DigDistMsgReader_ScrollReaderDetermineClickCoordAction(pScrollRetObj, pEnhReadHelpLineClickCoords) -{ - var retObj = { - actionStr: "" - }; - - for (var coordIdx = 0; coordIdx < pEnhReadHelpLineClickCoords.length; ++coordIdx) - { - if ((pScrollRetObj.mouse.x == pEnhReadHelpLineClickCoords[coordIdx].x) && (pScrollRetObj.mouse.y == pEnhReadHelpLineClickCoords[coordIdx].y)) - { - // The up arrow, down arrow, PageUp, PageDown, Home, and End aren't handled - // here - Those are handled in scrollTextLines(). - if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == LEFT_ARROW) - retObj.actionStr = this.enhReaderKeys.previousMsg; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == RIGHT_ARROW) - retObj.actionStr = this.enhReaderKeys.nextMsg; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("DEL") == 0) - retObj.actionStr = this.enhReaderKeys.deleteMessage; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("E)") == 0) - retObj.actionStr = this.enhReaderKeys.editMsg; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("F") == 0) - retObj.actionStr = this.enhReaderKeys.firstMsg; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("L") == 0) - retObj.actionStr = this.enhReaderKeys.lastMsg; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("R") == 0) - retObj.actionStr = this.enhReaderKeys.reply; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("C") == 0) - retObj.actionStr = this.enhReaderKeys.chgMsgArea; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("Q") == 0) - retObj.actionStr = this.enhReaderKeys.quit; - else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("?") == 0) - retObj.actionStr = this.enhReaderKeys.showHelp; - break; - } - } - return retObj; -} -// Helper method for ReadMessageEnhanced() - Does the traditional (non-scrollable) reader interface -function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, pmode) -{ - var retObj = { - offsetValid: true, - msgNotReadable: false, - userReplied: false, - lastKeypress: "", - newMsgOffset: -1, - nextAction: ACTION_NONE, - refreshEnhancedRdrHelpLine: false - }; - - // We could word-wrap the message to ensure words aren't split across lines, but - // doing so could make some messages look bad (i.e., messages with drawing characters), - // and word_wrap also might not handle ANSI or other color/attribute codes.. - //if (!textHasDrawingChars(messageText)) - // messageText = word_wrap(messageText, this.msgAreaWidth); - - var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader); - - // Only interpret @-codes if the user is reading personal email and the sender is a sysop. 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 && msgSenderIsASysop(msgHeader)) - messageText = replaceAtCodesInStr(messageText); // Or this.ParseMsgAtCodes(messageText, msgHeader) to replace only some @ codes - var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0; - var msgTextWrapped = (msgHasANSICodes ? messageText : word_wrap(messageText, console.screen_columns-1)); - - // Generate the key help text - var keyHelpText = "\x01n\x01c\x01h#\x01n\x01b, \x01c\x01hLeft\x01n\x01b, \x01c\x01hRight\x01n\x01b, "; - if (this.CanDelete() || this.CanDeleteLastMsg()) - keyHelpText += "\x01c\x01hDEL\x01b, "; - if (this.CanEdit()) - keyHelpText += "\x01c\x01hE\x01y)\x01n\x01cdit\x01b, "; - keyHelpText += "\x01c\x01hF\x01y)\x01n\x01cirst\x01b, \x01c\x01hL\x01y)\x01n\x01cast\x01b, \x01c\x01hR\x01y)\x01n\x01ceply\x01b, "; - // 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 += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, "; - keyHelpText += "\x01c\x01hC\x01y)\x01n\x01chg area\x01b, "; - } - else - { - // The user isn't allowed to change to a different message area. - // Go ahead and include the private reply option. - keyHelpText += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, "; - } - keyHelpText += "\x01c\x01hQ\x01y)\x01n\x01cuit\x01b, \x01c\x01h?\x01g: \x01c"; - - // For showing the message hex dump (for the sysop) - var msgHexInfo = null; - - // User input loop - var writeMessage = true; - var writePromptText = true; - var continueOn = true; - while (continueOn) - { - if (writeMessage) - { - if (console.term_supports(USER_ANSI)) - console.clear("\x01n"); - // Write the message header & message body to the screen - this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1); - console.print("\x01n" + this.colors.msgBodyColor); - console.putmsg(msgTextWrapped, pmode|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); - retObj.lastKeypress = getKeyWithESCChars(K_UPPER); - switch (retObj.lastKeypress) - { - case this.enhReaderKeys.deleteMessage: // Delete message - case '\x7f': - case '\x08': - console.crlf(); - // Prompt the user for confirmation to delete the message. - // Note: this.PromptAndDeleteOrUndeleteMessage() 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.PromptAndDeleteOrUndeleteMessage(pOffset, null, true); - if (msgWasDeleted && !canViewDeletedMsgs()) - { - var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); - continueOn = msgSearchObj.continueInputLoop; - retObj.newMsgOffset = msgSearchObj.newMsgOffset; - retObj.nextAction = msgSearchObj.nextAction; - if (msgSearchObj.promptGoToNextArea) - { - if (console.yesno(replaceAtCodesInStr(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 this.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 this.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.DeleteOrUndeleteSelectedMessages() 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 this.enhReaderKeys.editMsg: // 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 this.enhReaderKeys.showHelp: // Show help - if (!console.term_supports(USER_ANSI)) - { - console.crlf(); - console.crlf(); - } - this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments); - if (!console.term_supports(USER_ANSI)) - { - console.crlf(); - console.crlf(); - } - break; - case this.enhReaderKeys.reply: // Reply to the message - case this.enhReaderKeys.privateReply: // Private reply - // If the user pressed the private reply key while reading private - // mail, then do nothing (allow only the regular reply key to reply). - // If not reading personal email, go ahead and let the user reply - // with either the reply or private reply keypress. - var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply); - 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(); - // Get the message header with fields expanded so we can get the most info possible. - //var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true); - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true); - msgbase.close(); - // Let the user reply to the message - var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset); - retObj.userReplied = replyRetObj.postSucceeded; - //retObj.msgNotReadable = replyRetObj.msgWasDeleted; - var msgWasDeleted = replyRetObj.msgWasDeleted; - if (msgWasDeleted && !canViewDeletedMsgs()) - { - var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset); - continueOn = msgSearchObj.continueInputLoop; - retObj.newMsgOffset = msgSearchObj.newMsgOffset; - retObj.nextAction = msgSearchObj.nextAction; - if (msgSearchObj.promptGoToNextArea) - { - if (console.yesno(replaceAtCodesInStr(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 // msgbase failed to open - { - console.attributes = "N"; - console.crlf(); - console.print("\x01h\x01yFailed to open the sub-board. Aborting.\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - } - break; - case this.enhReaderKeys.postMsg: // 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, replaceAtCodesInStr(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("\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n"); - console.crlf(); - console.pause(); - } - else - { - // Confirm with the user whether to read the message - var readMsg = true; - if (this.promptToReadMessage) - { - readMsg = console.yesno("\x01n" + 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 this.enhReaderKeys.prevMsgByTitle: // Previous message by title - case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author - case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user' - case this.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, this.enhReaderKeys), - false); - if (threadPrevMsgOffset > -1) - { - retObj.newMsgOffset = threadPrevMsgOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - } - } - else - { - writeMessage = false; - writePromptText = false; - } - break; - case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject) - case this.enhReaderKeys.nextMsgByAuthor: // Next message by author - case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user' - case this.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, this.enhReaderKeys), - false); - if (threadNextMsgOffset > -1) - { - retObj.newMsgOffset = threadNextMsgOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - } - } - else - { - writeMessage = false; - writePromptText = false; - } - break; - case this.enhReaderKeys.previousMsg: // 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.FindNextReadableMsgIdx(pOffset, false); - var goToPrevMessage = false; - if ((retObj.newMsgOffset > -1) || allowChgMsgArea) - { - if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast()) - { - console.crlf(); - goToPrevMessage = console.yesno(replaceAtCodesInStr(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 this.enhReaderKeys.nextMsg: // 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.FindNextReadableMsgIdx(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.attributes = "N"; - console.crlf(); - // If configured to allow the user to post in the sub-board - // instead of going to the next message area and we're not - // scanning, then do so. - if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan) - { - // Ask the user if they want to post on the sub-board. - // If they say yes, then do so before exiting. - var grpNameAndDesc = this.GetGroupNameAndDesc(); - if (!console.noyes(replaceAtCodesInStr(format(this.text.postOnSubBoard, grpNameAndDesc.grpName, grpNameAndDesc.grpDesc)))) - bbs.post_msg(this.subBoardCode); - continueOn = false; - retObj.nextAction = ACTION_QUIT; - } - else - { - if (console.yesno(replaceAtCodesInStr(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 this.enhReaderKeys.firstMsg: // 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 this.enhReaderKeys.lastMsg: // 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 this.enhReaderKeys.showHdrInfo: - case this.enhReaderKeys.showKludgeLines: - if (user.is_sysop) - { - console.crlf(); - // Get an array of the extended header info/kludge lines and then - // display them. - var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines)); - 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(replaceAtCodesInStr(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 this.enhReaderKeys.showMsgList: // Message list - console.attributes = "N"; - console.crlf(); - console.print("Loading..."); - retObj.nextAction = ACTION_DISPLAY_MSG_LIST; - continueOn = false; - break; - case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed - if (allowChgMsgArea) - { - retObj.nextAction = ACTION_CHG_MSG_AREA; - continueOn = false; - } - else - { - writeMessage = false; - writePromptText = false; - } - break; - case this.enhReaderKeys.downloadAttachments: // Download attachments - if (msgHasAttachments) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01c- Download Attached Files -\x01n"); - allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode); - - // Ensure the message is refreshed on the screen - writeMessage = true; - writePromptText = true; - } - else - { - writeMessage = false; - writePromptText = false; - } - break; - case this.enhReaderKeys.saveToBBSMachine: - // Save the message to the BBS machine - Only allow this - // if the user is a sysop. - if (user.is_sysop) - { - console.crlf(); - console.print("\x01n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - console.crlf(); - if (filename.length > 0) - { - var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename); - if (saveMsgRetObj.succeeded) - { - console.print("\x01n\x01cThe message has been saved.\x01n"); - if (msgHdrHasAttachmentFlag(msgHeader)) - console.print(" Attachments not saved."); - console.attributes = "N"; - } - else - console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - else - { - console.print("\x01n\x01y\x01hMessage not exported\x01n"); - mswait(ERROR_PAUSE_WAIT_MS); - } - writeMessage = true; - } - else - writeMessage = false; - break; - case this.enhReaderKeys.userEdit: // Edit the user who wrote the message - if (user.is_sysop) - { - console.attributes = "N"; - console.crlf(); - console.print("- Edit user " + msgHeader.from); - console.crlf(); - var editObj = editUser(msgHeader.from); - if (editObj.errorMsg.length != 0) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01y\x01h" + editObj.errorMsg + "\x01n"); - console.crlf(); - console.pause(); - } - } - writeMessage = true; - break; - case this.enhReaderKeys.forwardMsg: // Forward the message - console.attributes = "N"; - console.crlf(); - console.print("\x01c- Forward message\x01n"); - console.crlf(); - var retStr = this.ForwardMessage(msgHeader, messageText); - if (retStr.length > 0) - { - console.print("\x01n\x01h\x01y* " + retStr + "\x01n"); - console.crlf(); - console.pause(); - } - writeMessage = true; - break; - case this.enhReaderKeys.vote: // Vote on the message - var voteRetObj = this.VoteOnMessage(msgHeader); - if (voteRetObj.BBSHasVoteFunction) - { - if (!voteRetObj.userQuit) - { - if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote)) - { - console.attributes = "N"; - console.crlf(); - if (voteRetObj.errorMsg.length > 0) - { - if (voteRetObj.mnemonicsRequiredForErrorMsg) - { - console.mnemonics(voteRetObj.errorMsg); - console.attributes = "N"; - } - else - console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n"); - } - else if (!voteRetObj.savedVote) - console.print("\x01y\x01h* Failed to save the vote\x01n"); - console.crlf(); - console.pause(); - } - else - msgHeader = voteRetObj.updatedHdr; // To get updated vote information - } - - // If this message is a poll, then exit out of the reader - // and come back to read the same message again so that the - // voting results are re-loaded and displayed on the screen. - if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL) - { - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - } - else - writeMessage = true; // We want to refresh the message on the screen - } - else - writeMessage = false; - break; - case this.enhReaderKeys.showVotes: // Show votes - if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes")) - { - console.attributes = "N"; - console.crlf(); - var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader); - for (var voteInfoIdx = 0; voteInfoIdx < voteInfo.length; ++voteInfoIdx) - { - console.print(voteInfo[voteInfoIdx]); - console.crlf(); - } - } - else - { - console.print("\x01n\x01h\x01yThere is no voting information for this message\x01n"); - console.crlf(); - } - console.pause(); - writeMessage = true; - break; - case this.enhReaderKeys.closePoll: // Close a poll message - var pollCloseMsg = ""; - console.attributes = "N"; - console.crlf(); - // If this message is a poll, then allow closing it. - if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL) - { - if ((msgHeader.auxattr & POLL_CLOSED) == 0) - { - // Only let the user close the poll if they created it - if (userHandleAliasNameMatch(msgHeader.from)) - { - // Prompt to confirm whether the user wants to close the poll - if (!console.noyes("Close poll")) - { - // Close the poll (open the sub-board first) - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if (closePollWithOpenMsgbase(msgbase, msgHeader.number)) - { - msgHeader.auxattr |= POLL_CLOSED; - pollCloseMsg = "\x01n\x01cThis poll was successfully closed."; - } - else - pollCloseMsg = "\x01n\x01r\x01h* Failed to close this poll!"; - msgbase.close(); - } - else - pollCloseMsg = "\x01n\x01r\x01h* Failed to open the sub-board!"; - } - } - else - pollCloseMsg = "\x01n\x01y\x01hCan't close this poll because it's not yours"; - } - else - pollCloseMsg = "\x01n\x01y\x01hThis poll is already closed"; - } - else - pollCloseMsg = "This message is not a poll"; - - // Display the poll closing status message - if (strip_ctrl(pollCloseMsg).length > 0) - { - console.print("\x01n" + pollCloseMsg + "\x01n"); - console.crlf(); - console.pause(); - } - writeMessage = true; - break; - case this.enhReaderKeys.validateMsg: // Validate the message - if (user.is_sysop && (this.subBoardCode != "mail") && msg_area.sub[this.subBoardCode].is_moderated) - { - var message = ""; - if (this.ValidateMsg(this.subBoardCode, msgHeader.number)) - { - message = "\x01n\x01cMessage validation successful"; - // Refresh the message header in the arrays - this.RefreshMsgHdrInArrays(msgHeader.number); - // Exit out of the reader and come back to read - // the same message again so that the voting results - // are re-loaded and displayed on the screen. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - } - else - { - message = "\x01n\x01y\x01hMessage validation failed!"; - writeMessage = true; - } - console.crlf(); - console.print(message + "\x01n"); - console.crlf(); - console.pause(); - } - else - writeMessage = false; - break; - case this.enhReaderKeys.quickValUser: // Quick-validate the user - if (user.is_sysop) - quickValidateLocalUser(msgHeader.from, false, this.quickUserValSetIndex); - else - writeMessage = false; - break; - case this.enhReaderKeys.bypassSubBoardInNewScan: - // TODO: Finish - writeMessage = false; - /* - if (this.doingMsgScan) - { - console.attributes = "N"; - console.crlf(); - if (!console.noyes("Bypass this sub-board in newscans")) - { - continueOn = false; - msg_area.sub[this.subBoardCode].scan_cfg &= SCAN_CFG_NEW; - } - else - writeMessage = true; - } - else - writeMessage = false; - */ - break; - case this.enhReaderKeys.userSettings: - var userSettingsRetObj = this.DoUserSettings_Traditional(); - // In case the user changed their twitlist, re-filter the messages for this sub-board - if (userSettingsRetObj.userTwitListChanged) - { - console.crlf(); - console.print("\x01nTwitlist changed; re-filtering.."); - var tmpMsgbase = new MsgBase(this.subBoardCode); - if (tmpMsgbase.open()) - { - continueOn = false; - writeMessage = false; - var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true); - tmpMsgbase.close(); - this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true); - // If the user is currently reading a message a message by someone who is now - // in their twit list, change the message currently being viewed. - if (this.MsgHdrFromOrToInUserTwitlist(msgHeader)) - { - var newReadableMsgOffset = this.FindNextReadableMsgIdx(pOffset, true); - if (newReadableMsgOffset > -1) - { - retObj.newMsgOffset = newReadableMsgOffset; - retObj.offsetValid = true; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - } - else - retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; - } - else - { - // If there are still messages in this sub-board, and the message offset is beyond the last - // message, then show the last message in the sub-board. Otherwise, go to the next message area. - if (this.hdrsForCurrentSubBoard.length > 0) - { - if (pOffset > this.hdrsForCurrentSubBoard.length) - { - //this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number - retObj.newMsgOffset = this.hdrsForCurrentSubBoard.length - 1; - retObj.offsetValid = true; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - } - } - else - retObj.nextAction = ACTION_GO_NEXT_MSG_AREA; - } - } - else - console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p"); - this.SetUpLightbarMsgListVars(); - writeMessage = true; - } - break; - case this.enhReaderKeys.showMsgHex: // Show message hex dump - writeMessage = false; - writePromptText = false; - if (user.is_sysop) - { - if (msgHexInfo == null) - msgHexInfo = this.GetMsgHexInfo(messageText, true); - if (msgHexInfo.msgHexArray.length > 0) - { - writeMessage = true; - writePromptText = true; - console.attributes = "N"; - console.crlf(); - console.print("\x01c\x01hMessage hex dump:\x01n\r\n"); - console.print("=================\r\n"); - for (var hexI = 0; hexI < msgHexInfo.msgHexArray.length; ++hexI) - console.print(msgHexInfo.msgHexArray[hexI] + "\r\n"); - console.pause(); - } - } - break; - case this.enhReaderKeys.hexDump: - writeMessage = false; - writePromptText = false; - // Save a hex dump of the message to the BBS machine - Only allow this - // if the user is a sysop. - if (user.is_sysop) - { - writeMessage = true; - writePromptText = true; - // Prompt the user for a filename to save the hex dump to the - // BBS machine - console.print("\x01n\r\n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - console.crlf(); - if (filename.length > 0) - { - var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename); - if (saveHexRetObj.saveSucceeded) - console.print("\x01n\x01cThe hex dump has been saved.\x01n"); - else if (saveHexRetObj.errorMsg != "") - console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n"); - else - console.print("\x01n\x01y\x01hFailed!\x01n"); - } - else - console.print("\x01n\x01y\x01hHex dump not exported\x01n"); - console.crlf(); - console.pause(); - } - break; - case this.enhReaderKeys.operatorMenu: // Operator menu - if (user.is_sysop) - { - writeMessage = true; - writePromptText = true; - console.crlf(); - console.print("\x01w\x01h== Operator menu ==\x01n"); - console.crlf(); - var opRetObj = this.ShowReadModeOpMenuAndGetSelection(); - if (opRetObj.chosenOption != null) - { - switch (opRetObj.chosenOption) - { - case this.readerOpMenuOptValues.validateMsg: // Validate the message - if (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated) - { - var message = ""; - if (this.ValidateMsg(this.subBoardCode, msgHeader.number)) - { - message = "\x01n\x01cMessage validation successful"; - // Refresh the message header in the arrays - this.RefreshMsgHdrInArrays(msgHeader.number); - // Exit out of the reader and come back to read - // the same message again so that the voting results - // are re-loaded and displayed on the screen. - retObj.newMsgOffset = pOffset; - retObj.nextAction = ACTION_GO_SPECIFIC_MSG; - continueOn = false; - } - else - message = "\x01n\x01y\x01hMessage validation failed!"; - console.crlf(); - console.print(message + "\x01n"); - console.crlf(); - console.pause(); - } - break; - case this.readerOpMenuOptValues.editAuthorUserAccount: // Edit author's user account - console.attributes = "N"; - console.crlf(); - console.print("- Edit user " + msgHeader.from); - console.crlf(); - var editObj = editUser(msgHeader.from); - if (editObj.errorMsg.length != 0) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01y\x01h" + editObj.errorMsg + "\x01n"); - console.crlf(); - console.pause(); - } - console.attributes = "N"; - break; - case this.readerOpMenuOptValues.showHdrLines: // Show header or kludge lines - case this.readerOpMenuOptValues.showKludgeLines: - console.crlf(); - // Get an array of the extended header info/kludge lines and then - // display them. - var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, (opRetObj.chosenOption == this.readerOpMenuOptValues.showKludgeLines)); - 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(replaceAtCodesInStr(this.text.noKludgeLinesForThisMsgText)); - console.crlf(); - console.pause(); - } - break; - case this.readerOpMenuOptValues.showTallyStats: // Show tally stats - if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes")) - { - console.attributes = "N"; - console.crlf(); - var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader); - for (var voteInfoIdx = 0; voteInfoIdx < voteInfo.length; ++voteInfoIdx) - { - console.print(voteInfo[voteInfoIdx]); - console.crlf(); - } - } - else - { - console.print("\x01n\x01h\x01yThere is no voting information for this message\x01n"); - console.crlf(); - } - console.pause(); - break; - case this.readerOpMenuOptValues.showMsgHex: - writeMessage = false; - writePromptText = false; - if (msgHexInfo == null) - msgHexInfo = this.GetMsgHexInfo(messageText, true); - if (msgHexInfo.msgHexArray.length > 0) - { - writeMessage = true; - writePromptText = true; - console.attributes = "N"; - console.crlf(); - console.print("\x01c\x01hMessage hex dump:\x01n\r\n"); - console.print("=================\r\n"); - for (var hexI = 0; hexI < msgHexInfo.msgHexArray.length; ++hexI) - console.print(msgHexInfo.msgHexArray[hexI] + "\r\n"); - console.pause(); - } - break; - case this.readerOpMenuOptValues.saveMsgHexToFile: - writeMessage = true; - writePromptText = true; - // Prompt the user for a filename to save the hex dump to the - // BBS machine - console.print("\x01n\r\n\x01cFilename:\x01h"); - var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1 - var filename = console.getstr(inputLen, K_NOCRLF); - console.attributes = "N"; - console.crlf(); - if (filename.length > 0) - { - var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename); - if (saveHexRetObj.saveSucceeded) - console.print("\x01n\x01cThe hex dump has been saved.\x01n"); - else if (saveHexRetObj.errorMsg != "") - console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n"); - else - console.print("\x01n\x01y\x01hFailed!\x01n"); - } - else - console.print("\x01n\x01y\x01hHex dump not exported\x01n"); - console.crlf(); - console.pause(); - break; - case this.readerOpMenuOptValues.quickValUser: // Quick-validate the user - quickValidateLocalUser(msgHeader.from, false, this.quickUserValSetIndex); - break; - case this.readerOpMenuOptValues.addAuthorToTwitList: // Add author to twit list - var promptTxt = format("Add %s to twit list", msgHeader.from); - if (!console.noyes(promptTxt)) - { - var statusMsg = "\x01n" + addToTwitList(msgHeader.from) ? "\x01w\x01hSuccessfully updated the twit list" : "\x01y\x01hFailed to update the twit list!" - console.print(statusMsg); - console.attributes = "N"; - console.crlf(); - console.pause(); - } - break; - case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter: - var fromEmailAddr = ""; - if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0) - { - if (msgHeader.from_net_type == NET_INTERNET) - fromEmailAddr = msgHeader.from_net_addr; - else - fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr; - } - var promptTxt = format("Add %s to global email filter", fromEmailAddr); - if (!console.noyes(promptTxt)) - { - var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!" - console.print(statusMsg); - console.attributes = "N"; - console.crlf(); - console.pause(); - } - break; - } - } - } - else - { - writeMessage = false; - writePromptText = false; - } - break; - case this.enhReaderKeys.quit: // Quit - case KEY_ESC: - // Normally, if quitFromReaderGoesToMsgList is enabled, then do that, except - // in indexed mode, allow going back to the indexed mode menu. - if (this.userSettings.quitFromReaderGoesToMsgList && !this.indexedMode) - { - console.attributes = "N"; - console.crlf(); - console.print("Loading..."); - retObj.nextAction = ACTION_DISPLAY_MSG_LIST; - } - else - retObj.nextAction = ACTION_QUIT; - continueOn = false; - break; - default: - // No need to do anything - writeMessage = false; - writePromptText = false; - break; - } - } - - return retObj; -} - -// For the DDMsgReader class: Does the operator mode for reading. -// -// Return value: An object with the following properties: -// menuTopLeftX: The horizontal component of the upper-left corner of the operator menu (for scrollable mode) -// menuTopLeftY: The vertical component of the upper-left corner of the operator menu (for scrollable mode) -// menuWidth: The width of the operator menu (for scrollable mode) -// menuHeight: The height of the operator menu (for scrollable mode) -// chosenOption: The user's chosen option from the menu (one of this.readerOpMenuOptValues), or null -// if the user quit/aborted -// lastUserInput: The user's last key input from the menu (empty string if there is none) -function DigDistMsgReader_ShowReadModeOpMenuAndGetSelection() -{ - var retObj = { - menuTopLeftX: 1, - menuTopLeftY: 1, - menuWidth: 0, - menuHeight: 0, - chosenOption: null, - lastUserInput: "" - }; - - // This is only for the sysop - if (!user.is_sysop) - return retObj; - - // If using scrollable mode, create the operator menu & display it - - var opMenu = this.CreateReadModeOpMenu(); - retObj.menuTopLeftX = opMenu.pos.x; - retObj.menuTopLeftY = opMenu.pos.y; - retObj.menuWidth = opMenu.size.width; - retObj.menuHeight = opMenu.size.height; - - //GetVal(pDraw, pSelectedItemIndexes) - retObj.chosenOption = opMenu.GetVal(); - if (typeof(opMenu.lastUserInput) === "string") - retObj.lastUserInput = opMenu.lastUserInput; - // If the user pressed one of the additional quit keys set up for the - // menu, make sure the chosen option is null (it should be anyway) - if (retObj.lastUserInput.toUpperCase() == "Q" || retObj.lastUserInput == KEY_ESC) // Quit - retObj.chosenOption = null; - - return retObj; -} - -// For the DDMsgReader class: Creates the operator menu for read mode. -// The menu should be created each time, because the options could be different for -// each sub-board (i.e., if the sub-board is moderated, then the 'Validate message' -// option will be available). -function DigDistMsgReader_CreateReadModeOpMenu() -{ - //var itemFormatStr = "\x01n\x01c\x01h%s\x01y"; - // We'll add the 'Validate message' option if the sub-board isn't email and is moderated - var subBoardIsModerated = (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated); - - var opMenuWidth = 35; - var opMenuHeight = 11; - if (subBoardIsModerated) - ++opMenuHeight; - var opMenuX = Math.floor(console.screen_columns/2) - Math.floor(opMenuWidth/2); - var opMenuY = this.msgAreaTop + 2; - var opMenu = new DDLightbarMenu(opMenuX, opMenuY, opMenuWidth, opMenuHeight); - opMenu.borderEnabled = true; - opMenu.allowANSI = this.scrollingReaderInterface && console.term_supports(USER_ANSI); - opMenu.topBorderText = "\x01n\x01w\x01hOperator menu (reader mode)"; - if (subBoardIsModerated) - opMenu.Add("&A: Validate the message", this.readerOpMenuOptValues.validateMsg); - // If the scrollbar/ANSI interface is being used, then add the menu items with hotkey characters. - // Otherwise don't use the hotkey characters (the menu will use numbered mode). - if (opMenu.allowANSI) - { - opMenu.Add("&U: Edit author's user account", this.readerOpMenuOptValues.editAuthorUserAccount); - opMenu.Add("&H: Show header lines", this.readerOpMenuOptValues.showHdrLines); - opMenu.Add("&K: Show kludge lines", this.readerOpMenuOptValues.showKludgeLines); - opMenu.Add("&T: Show tally stats", this.readerOpMenuOptValues.showTallyStats); - opMenu.Add("&X: Show message hex", this.readerOpMenuOptValues.showMsgHex); - opMenu.Add("&E: Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile); - opMenu.Add("&A: Quick validate the user", this.readerOpMenuOptValues.quickValUser); - opMenu.Add("&I: Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList); - opMenu.Add("&M: Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter); - // Use cyan for the item color, and cyan with blue background for selected item color - opMenu.colors.itemColor = "\x01n\x01c"; - opMenu.colors.selectedItemColor = "\x01n\x01c\x014"; - } - else - { - opMenu.Add("Edit author's user account", this.readerOpMenuOptValues.editAuthorUserAccount); - opMenu.Add("Show header lines", this.readerOpMenuOptValues.showHdrLines); - opMenu.Add("Show kludge lines", this.readerOpMenuOptValues.showKludgeLines); - opMenu.Add("Show tally stats", this.readerOpMenuOptValues.showTallyStats); - opMenu.Add("Show message hex", this.readerOpMenuOptValues.showMsgHex); - opMenu.Add("Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile); - opMenu.Add("Quick validate the user", this.readerOpMenuOptValues.quickValUser); - opMenu.Add("Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList); - opMenu.Add("Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter); - // Use green for the item color and high cyan for the item number color - opMenu.colors.itemColor = "\x01n\x01g"; - opMenu.colors.itemNumColor = "\x01n\x01c\x01h"; - } - - opMenu.AddAdditionalQuitKeys("qQ" + this.enhReaderKeys.operatorMenu); - - return opMenu; -} - -// For the ReadMessageEnhanced methods: This function converts a thread navigation -// key character to its corresponding thread type value -function keypressToThreadType(pKeypress, pEnhReaderKeys) -{ - var threadType = THREAD_BY_ID; - switch (pKeypress) - { - case pEnhReaderKeys.prevMsgByTitle: - case pEnhReaderKeys.nextMsgByTitle: - threadType = THREAD_BY_TITLE; - break; - case pEnhReaderKeys.prevMsgByAuthor: - case pEnhReaderKeys.nextMsgByAuthor: - threadType = THREAD_BY_AUTHOR; - break; - case pEnhReaderKeys.prevMsgByToUser: - case pEnhReaderKeys.nextMsgByToUser: - threadType = THREAD_BY_TO_USER; - break; - case pEnhReaderKeys.prevMsgByThreadID: - case pEnhReaderKeys.nextMsgByThreadID: - default: - threadType = THREAD_BY_ID; - break; - } - return threadType; -} - -// 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("\x01n" + 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.attributes = "N"; - 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 = { - newMsgOffset: 0, - nextAction: ACTION_NONE, - continueInputLoop: true, - 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.FindNextReadableMsgIdx(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.FindNextReadableMsgIdx(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); - // TODO: Mouse: console.print replaced with console.putmsg for mouse click hotspots - //console.print(displayChgAreaOpt ? this.enhReadHelpLine : this.enhReadHelpLineWithoutChgArea); - // console.putmsg() handles @-codes, which we use for mouse click tracking - console.putmsg(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 -// pPromptPrevIfNoResults: Optional boolean - Whether or not to prompt the user to -// go to the previous area if there are no search results. -// -// 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, pPromptPrevIfNoResults) -{ - var retObj = { - changedMsgArea: false, - msgIndex: -1, - 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; - this.setSubBoardCode(readMsgRetObj.subCode); - 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. - // We probably shouldn't use GetMsgIdx() yet because the arrays of - // message headers have not been populated for the next area yet - retObj.msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read); - //retObj.msgIndex = this.GetMsgIdx(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 readableMsgIdx = this.FindNextReadableMsgIdx(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 (readableMsgIdx > -1) - retObj.msgIndex = readableMsgIdx; - 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 - { - var promptPrevIfNoResults = (typeof(pPromptPrevIfNoResults) === "boolean" ? pPromptPrevIfNoResults : true); - if (promptPrevIfNoResults) - continueGoingToPrevSubBoard = !console.noyes("Continue searching"); - if (!continueGoingToPrevSubBoard) - { - retObj.shouldStopReading = true; - return retObj; - } - } - } - else - { - retObj.changedMsgArea = true; - this.PopulateHdrsForCurrentSubBoard(); - if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI)) - this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea); - } - } - 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, - "\x01n\x01h\x01y* No prior messages or no message in prior message areas.", - ERROR_PAUSE_WAIT_MS, "\x01n", 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 -// pPromptNextIfNoResults: Optional boolean - Whether or not to prompt the user to -// go to the next area if there are no search results. -// Defaults to true. -// -// 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, pPromptNextIfNoResults) -{ - var retObj = { - changedMsgArea: false, - msgIndex: -1, - 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; - this.setSubBoardCode(readMsgRetObj.subCode); - 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); - // We probably shouldn't use GetMsgIdx() yet because the arrays of - // message headers have not been populated for the next area yet - //retObj.msgIndex = this.GetMsgIdx(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 readableMsgIdx = this.FindNextReadableMsgIdx(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 (readableMsgIdx > -1) - { - retObj.msgIndex = readableMsgIdx; - retObj.changedMsgArea = true; - var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx); - 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; - this.PopulateHdrsForCurrentSubBoard(); - 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 - { - var promptNextIfNoresults = (typeof(pPromptNextIfNoResults) === "boolean" ? pPromptNextIfNoResults : true); - if (promptNextIfNoresults) - continueGoingToNextSubBoard = !console.noyes("Continue searching"); - if (!continueGoingToNextSubBoard) - { - retObj.shouldStopReading = true; - return retObj; - } - } - } - else - { - // There is no search. Populate the arrays of all headers - // for this sub-board - this.PopulateHdrsForCurrentSubBoard(); - retObj.msgIndex = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read); - if (retObj.msgIndex == -1) - retObj.msgIndex = 0; - } - } - 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, - "\x01n\x01h\x01y* No more messages or message areas.", - ERROR_PAUSE_WAIT_MS, "\x01n", 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.GetLastReadMsgIdxAndNum().lastReadMsgIdx; - if (lastReadMsgIdx == -1) - lastReadMsgIdx = 0; - } - var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.tradMsgListNumLines, this.NumMessages(), - this.userSettings.listMessagesInReverse); - this.CalcTraditionalMsgListTopIdx(pageNum); - if (!this.userSettings.listMessagesInReverse && (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.GetLastReadMsgIdxAndNum().lastReadMsgIdx; - 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.GetLastReadMsgIdxAndNum().lastReadMsgIdx; - if (lastReadMsgIdx == -1) - lastReadMsgIdx = 0; - } - } - var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.lightbarMsgListNumLines, this.NumMessages(), - this.userSettings.listMessagesInReverse); - this.CalcLightbarMsgListTopIdx(pageNum); - var initialCursorRow = 0; - if (this.userSettings.listMessagesInReverse) - initialCursorRow = this.lightbarMsgListStartScreenRow+(this.lightbarListTopMsgIdx-lastReadMsgIdx); - else - { - if (this.lightbarListTopMsgIdx > lastReadMsgIdx) - this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines; - initialCursorRow = this.lightbarMsgListStartScreenRow+(lastReadMsgIdx-this.lightbarListTopMsgIdx); - } - if (this.userSettings.listMessagesInReverse) - { - this.lightbarListSelectedMsgIdx = this.NumMessages() - lastReadMsgIdx - 1; - if (this.lightbarListSelectedMsgIdx < 0) - this.lightbarListSelectedMsgIdx = 0; - } - else - 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 & sub-board name - // For the message group name, we can also use msgbase.cfg.grp_name in - // Synchronet 3.12 and higher. - var msgbase = new MsgBase(this.subBoardCode); - var msgGroupName = ""; - if (this.subBoardCode == "mail") - msgGroupName = "Mail"; - else - msgGroupName = msg_area.grp_list[msgbase.cfg.grp_number].description; - var subBoardName = "Unspecified"; - if (msgbase.open()) - { - if (msgbase.cfg != null) - subBoardName = msgbase.cfg.description; - else if ((msgbase.subnum == -1) || (msgbase.subnum == 65535)) - subBoardName = "Electronic Mail"; - else - subBoardName = "Unspecified"; - msgbase.close(); - } - - // 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 - if (this.showScoresInMsgList) - printf(this.colors.msgListColHeader + this.sMsgListHdrFormatStr, "Msg#", "From", "To", "Subject", "+/-", "Date", "Time"); - else - printf(this.colors.msgListColHeader + this.sMsgListHdrFormatStr, "Msg#", "From", "To", "Subject", "Date", "Time"); - - // Set the normal text attribute - console.attributes = "N"; -} -// 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.userSettings.listMessagesInReverse) - { - 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, this.showScoresInMsgList); - 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. - var msgHeader = this.GetMsgHdrByIdx(msgIndex, this.showScoresInMsgList); - 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.aborted) - console.pause(); - - console.aborted = false; -} -// 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("\x01n\x01cCurrently reading \x01g" + 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") - { - var numOfMessages = 0; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - numOfMessages = msgbase.total_msgs; - msgbase.close(); - } - console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages 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("\x01n\x01c"); - if (this.readingPersonalEmail) - console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + "."); - else - { - if (numSearchResults == 1) - console.print("There is \x01g1 \x01csearch result."); - else - console.print("There are \x01g" + numSearchResults + " \x01csearch results."); - } - console.crlf(); - } - console.crlf(); - - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor); - displayTextWithLineBelow("Page navigation and message selection", false, - this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h"); - 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, "\x01k\x01h"); - console.print(this.colors.tradInterfaceHelpScreenColor); - console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n"); - console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n"); - console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n"); - console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n"); - console.print("\x01n\x01h\x01cG" + 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("\x01n\x01h\x01cNumber" + 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("\x01n\x01h\x01cD" + this.colors.tradInterfaceHelpScreenColor + ": Mark a message for deletion\r\n"); - if (this.CanEdit()) - console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit an existing message\r\n"); - if (pChgSubBoardAllowed) - console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n"); - console.print("\x01n\x01h\x01cS" + this.colors.tradInterfaceHelpScreenColor + ": Select messages (for batch delete, etc.)\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " A message number or multiple numbers can be entered separated by commas or\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " spaces. Additionally, a range of numbers (separated by a dash) can be used.\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " Examples:\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " 125\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " 1,2,3\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " 1 2 3\r\n"); - console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + " 1,2,10-20\r\n"); - console.print("\x01n\x01h\x01cCTRL-U" + this.colors.tradInterfaceHelpScreenColor + ": Change your user settings\r\n"); - console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n"); - console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n"); - if (this.indexedMode) - console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n"); - console.print("\x01n\x01h\x01c?" + 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.aborted) - console.pause(); - - // Don't set this here - This is only ever called by DisplayMsgListHelp() - //console.aborted = false; -} -// 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("\x01n\x01cCurrently reading \x01g" + 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") - { - var numOfMessages = 0; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - numOfMessages = msgbase.total_msgs; - msgbase.close(); - } - console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board."); - 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("\x01n\x01c"); - if (this.readingPersonalEmail) - console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + "."); - else - { - if (numSearchResults == 1) - console.print("There is \x01g1 \x01csearch result."); - else - console.print("There are \x01g" + numSearchResults + " \x01csearch results."); - } - console.crlf(); - } - console.crlf(); - - displayTextWithLineBelow("Lightbar interface: Page navigation and message selection", - false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h"); - 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, "\x01k\x01h"); - console.print(this.colors.tradInterfaceHelpScreenColor); - console.print("\x01n\x01h\x01cDown arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor down/select the next message\r\n"); - console.print("\x01n\x01h\x01cUp arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor up/select the previous message\r\n"); - console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n"); - console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n"); - console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n"); - console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n"); - console.print("\x01n\x01h\x01cG" + 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("\x01n\x01h\x01cENTER" + this.colors.tradInterfaceHelpScreenColor + ": Read the selected message\r\n"); - console.print("\x01n\x01h\x01cNumber" + this.colors.tradInterfaceHelpScreenColor + ": Read the message corresponding with that number\r\n"); - if (this.CanDelete() || this.CanDeleteLastMsg()) - console.print("\x01n\x01h\x01cDEL" + this.colors.tradInterfaceHelpScreenColor + ": Mark the selected message for deletion\r\n"); - if (this.CanEdit()) - console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit the selected message\r\n"); - if (pChgSubBoardAllowed) - console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n"); - console.print("\x01n\x01h\x01cSpacebar" + this.colors.tradInterfaceHelpScreenColor + ": Select message (for batch delete, etc.)\r\n"); - console.print("\x01n\x01h\x01cCTRL-A" + this.colors.tradInterfaceHelpScreenColor + ": Select/de-select all messages\r\n"); - console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n"); - console.print("\x01n\x01h\x01cCTRL-U" + this.colors.tradInterfaceHelpScreenColor + ": Change your user settings\r\n"); - console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n"); - if (this.indexedMode) - console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n"); - console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n"); - - // If pPauseAtEnd is true, then pause. - if (pPauseAtEnd && !console.aborted) - console.pause(); - - // Don't set this here - This is only ever called by DisplayMsgListHelp() - //console.aborted = false; -} -// 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"], "\x01n\x01k\x01h") - console.print(this.colors.tradInterfaceHelpScreenColor); - var helpLines = [ - "Between the message number and 'From' name, a message could have the following status indicators:", - "\x01n\x01h\x01r\x01i" + this.msgListStatusChars.deleted + "\x01n" + this.colors.tradInterfaceHelpScreenColor + ": Message has been marked for deletion", - this.msgListStatusChars.attachments + ": The message has attachments", - this.msgListStatusChars.unread + ": The message is unread" - ]; - if (this.userSettings.displayMsgRepliedChar) - helpLines.push(this.msgListStatusChars.replied + ": You have replied to the message"); - var wrapLen = console.screen_columns-1; - for (var i = 0; i < helpLines.length; ++i) - { - var wrappedLines = word_wrap(helpLines[i], wrapLen).split("\n"); - for (var j = 0; j < wrappedLines.length; ++j) - { - if (wrappedLines[j].length == 0) continue; - console.print(wrappedLines[j] + "\r\n"); - } - } -} -// 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 the MsgBase object 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() -{ - - - // 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 = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "ext, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "ast, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] + "o, "; - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; - } - if (this.CanEdit()) - { - this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; - } - this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "el, "; - this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" + - this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] - + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " - + this.colors["tradInterfaceContPromptUserInputColor"]; - - this.sContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "ext, \x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "rev, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "irst, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "ast, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] + "o, "; - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; - } - if (this.CanEdit()) - { - this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; - } - this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "el, "; - this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" - + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] - + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " - + this.colors["tradInterfaceContPromptUserInputColor"]; - - this.sEndContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "rev, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "irst, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] + "o, "; - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; - } - if (this.CanEdit()) - { - this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; - } - this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "el, "; - this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" - + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"] - + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": " - + this.colors["tradInterfaceContPromptUserInputColor"]; - - this.msgListOnlyOnePageContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] + "o, "; - if (this.CanDelete() || this.CanDeleteLastMsg()) - { - this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, "; - } - if (this.CanEdit()) - { - this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] - + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, "; - } - this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)" - + this.colors["tradInterfaceContPromptMainColor"] - + "el, "; - this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)" - + 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. The @-codes are for mouse click tracking. - // For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead. \x1b[V seems to work though. - this.msgListLightbarModeHelpLine = this.colors.lightbarMsgListHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + ", " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + ", " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + "/" - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + ", " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`ENTER`" + KEY_ENTER + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + ", " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@" - + this.colors.lightbarMsgListHelpLineGeneralColor + ", " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`END`" + KEY_END + "@"; - var lbHelpLineLen = 31; - // 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`" + KEY_DEL + "@"; - lbHelpLineLen += 5; - } - this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineGeneralColor - + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor - + "#" + this.colors.lightbarMsgListHelpLineGeneralColor + ", "; - lbHelpLineLen += 5; - // If the user can edit messages, then append E as a valid key. - if (this.CanEdit()) - { - this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor - + "@`E`E@" + this.colors.lightbarMsgListHelpLineParenColor - + ")" + this.colors.lightbarMsgListHelpLineGeneralColor - + "dit, "; - lbHelpLineLen += 7; - } - this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor + "@`G`G@" - + this.colors.lightbarMsgListHelpLineParenColor + ")" - + this.colors.lightbarMsgListHelpLineGeneralColor + "o, " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`Q`Q@" - + this.colors.lightbarMsgListHelpLineParenColor + ")" - + this.colors.lightbarMsgListHelpLineGeneralColor + "uit, " - + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`?`?@ "; - lbHelpLineLen += 15; - - // 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 - 1; - if (numChars > 0) - { - // 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 = "\x01n" - + this.colors.lightbarMsgListHelpLineBkgColor - + this.msgListLightbarModeHelpLine; - this.msgListLightbarModeHelpLine += "\x01n" + 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() -{ - // For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead. \x1b[V seems to work though. - this.enhReadHelpLine = this.colors.enhReaderHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + LEFT_ARROW + "`" + KEY_LEFT + "@" - + this.colors.enhReaderHelpLineGeneralColor +", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + RIGHT_ARROW + "`" + KEY_RIGHT + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@" - + this.colors.enhReaderHelpLineGeneralColor + "/" - + this.colors.enhReaderHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`END`" + KEY_END + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor; - if (this.CanDelete() || this.CanDeleteLastMsg()) - this.enhReadHelpLine += "@`DEL`" + KEY_DEL + "@" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; - if (this.CanEdit() && (console.screen_columns > 87)) - this.enhReadHelpLine += "@`E`E@" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor; - this.enhReadHelpLine += "@`F`F@" + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "irst, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`L`L@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "ast, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`R`R@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "eply, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`C`C@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "hg area, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`Q`Q@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "uit, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`?`?@"; - // Center the help text based on the console width - //var numHotkeyChars = 92; - var numHotkeyChars = 89; - //var numCharsRemaining = console.screen_columns - (console.strlen(this.enhReadHelpLine) - numHotkeyChars) - 1; - var helpLineScreenLen = (console.strlen(this.enhReadHelpLine) - numHotkeyChars); - var numCharsRemaining = console.screen_columns - helpLineScreenLen - 1; - var frontPaddingLen = Math.floor(numCharsRemaining/2); - var padding = format("%*s", frontPaddingLen, ""); - this.enhReadHelpLine = padding + this.enhReadHelpLine; - this.enhReadHelpLine = "\x01n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLine; - if (console.screen_columns > 80) - { - //helpLineScreenLen += frontPaddingLen; - //numCharsRemaining = console.screen_columns - helpLineScreenLen - 1; - helpLineScreenLen = (console.strlen(this.enhReadHelpLine) - numHotkeyChars); - //numCharsRemaining = console.screen_columns - helpLineScreenLen - 2; - // Adding 3 as a correction factor for wide terminals (this is a kludge) - numCharsRemaining = console.screen_columns - helpLineScreenLen + 3; - if (numCharsRemaining > 0) - { - //this.enhReadHelpLine += format("%" + numCharsRemaining + "s", ""); - this.enhReadHelpLine += format("%*s", numCharsRemaining, ""); - } - } - - // Create a version without the change area option - // For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead. \x1b[V seems to work though. - this.enhReadHelpLineWithoutChgArea = this.colors.enhReaderHelpLineHotkeyColor + "@CLEAR_HOT@ @`" + UP_ARROW + "`" + KEY_UP + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + LEFT_ARROW + "`" + KEY_LEFT + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`" + RIGHT_ARROW + "`" + KEY_RIGHT + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@" - + this.colors.enhReaderHelpLineGeneralColor + "/" - + this.colors.enhReaderHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor + "@`END`" + KEY_END + "@" - + this.colors.enhReaderHelpLineGeneralColor + ", " - + this.colors.enhReaderHelpLineHotkeyColor; - if (this.CanDelete() || this.CanDeleteLastMsg()) - this.enhReadHelpLineWithoutChgArea += "@`DEL`" + KEY_DEL + "@" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor; - if (this.CanEdit()) - this.enhReadHelpLineWithoutChgArea += "@`E`E@" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor; - this.enhReadHelpLineWithoutChgArea += "@`F`F@" + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "irst, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`L`L@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "ast, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`R`R@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "eply, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`Q`Q@" - + this.colors.enhReaderHelpLineParenColor + ")" - + this.colors.enhReaderHelpLineGeneralColor + "uit, " - + this.colors.enhReaderHelpLineHotkeyColor + "@`?`?@ "; - - // Center the help text based on the console width - numHotkeyChars = 84; - //var numCharsRemaining = console.screen_columns - (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars) - 1; - helpLineScreenLen = (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars); - numCharsRemaining = console.screen_columns - helpLineScreenLen - 1; - if (numCharsRemaining > 0) - { - frontPaddingLen = Math.floor(numCharsRemaining/2); - //padding = format("%" + frontPaddingLen + "s", ""); - padding = format("%*s", frontPaddingLen, ""); - this.enhReadHelpLineWithoutChgArea = padding + this.enhReadHelpLineWithoutChgArea; - } - this.enhReadHelpLineWithoutChgArea = "\x01n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLineWithoutChgArea; - if (console.screen_columns > 80) - { - helpLineScreenLen = (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars); - // Adding 3 as a correction factor for wide terminals (this is a kludge) - numCharsRemaining = console.screen_columns - helpLineScreenLen + 3; - if (numCharsRemaining > 0) - { - //this.enhReadHelpLineWithoutChgArea += format("%" + numCharsRemaining + "s", ""); - this.enhReadHelpLineWithoutChgArea += format("%*s", numCharsRemaining, ""); - } - } -} -function stripCtrlFromEnhReadHelpLine_ReplaceArrowChars(pHelpLine) -{ - var helpLineNoAttrs = strip_ctrl(pHelpLine); - var charsToPutBack = [UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW]; - var helpLineIdx = -1; - for (var i = 0; i < charsToPutBack.length; ++i) - { - helpLineIdx = helpLineNoAttrs.indexOf(",", helpLineIdx+1); - if (helpLineIdx > -1) - { - helpLineNoAttrs = helpLineNoAttrs.substr(0, helpLineIdx) + charsToPutBack[i] + helpLineNoAttrs.substr(helpLineIdx); - ++helpLineIdx; - } - } - return helpLineNoAttrs; -} -// 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 = file_cfgname(system.mods_dir, this.cfgFilename); - if (!file_exists(cfgFilename)) - cfgFilename = file_cfgname(system.ctrl_dir, this.cfgFilename); - if (!file_exists(cfgFilename)) - cfgFilename = file_cfgname(gStartupPath, this.cfgFilename); - var cfgFile = new File(cfgFilename); - if (cfgFile.open("r")) - { - this.cfgFileSuccessfullyRead = true; - var settingsObj = cfgFile.iniGetObject(); - cfgFile.close(); - - var numSpaces = parseInt(settingsObj["tabSpaces"]); - if (!isNaN(numSpaces) && numSpaces > 0) - this.numTabSpaces = numSpaces; - var maxNumLines = parseInt(settingsObj["areaChooserHdrMaxLines"]); - if (!isNaN(maxNumLines) && maxNumLines > 0) - this.areaChooserHdrMaxLines = maxNumLines; - var numberVal = parseInt(settingsObj["quickUserValSetIndex"]); - if (!isNaN(numberVal) && numberVal > -1) - this.quickUserValSetIndex = numberVal; - if (settingsObj.hasOwnProperty("listInterfaceStyle") && typeof(settingsObj.listInterfaceStyle) === "string") - this.msgListUseLightbarListInterface = (settingsObj.listInterfaceStyle.toUpperCase() == "LIGHTBAR"); - if (typeof(settingsObj["readerInterfaceStyle"]) === "string") - this.scrollingReaderInterface = (settingsObj.readerInterfaceStyle.toUpperCase() == "SCROLLABLE"); - if (typeof(settingsObj["displayBoardInfoInHeader"]) === "boolean") - this.displayBoardInfoInHeader = settingsObj.displayBoardInfoInHeader; - if (typeof(settingsObj["promptToContinueListingMessages"]) === "boolean") - this.promptToContinueListingMessages = settingsObj.promptToContinueListingMessages; - if (typeof(settingsObj["promptConfirmReadMessage"]) === "boolean") - this.promptToReadMessage = settingsObj.promptConfirmReadMessage; - if (typeof(settingsObj["msgListDisplayTime"]) === "string") - this.msgList_displayMessageDateImported = (settingsObj.msgListDisplayTime.toUpperCase() == "IMPORTED"); - if (typeof(settingsObj["msgAreaList_lastImportedMsg_time"]) === "string") - this.msgAreaList_lastImportedMsg_showImportTime = (settingsObj.msgAreaList_lastImportedMsg_time.toUpperCase() == "IMPORTED"); - if (typeof(settingsObj["startMode"]) === "string") - { - var valueUpper = settingsObj.startMode.toUpperCase(); - if ((valueUpper == "READER") || (valueUpper == "READ")) - this.startMode = READER_MODE_READ; - else if ((valueUpper == "LISTER") || (valueUpper == "LIST")) - this.startMode = READER_MODE_LIST; - } - if (typeof(settingsObj["pauseAfterNewMsgScan"]) === "boolean") - this.pauseAfterNewMsgScan = settingsObj.pauseAfterNewMsgScan; - if (typeof(settingsObj["readingPostOnSubBoardInsteadOfGoToNext"]) === "boolean") - this.readingPostOnSubBoardInsteadOfGoToNext = settingsObj.readingPostOnSubBoardInsteadOfGoToNext; - if (typeof(settingsObj["areaChooserHdrFilenameBase"]) === "string") - this.areaChooserHdrFilenameBase = settingsObj.areaChooserHdrFilenameBase; - if (typeof(settingsObj["displayAvatars"]) === "boolean") - this.displayAvatars = settingsObj.displayAvatars; - if (typeof(settingsObj["rightJustifyAvatars"]) === "boolean") - this.rightJustifyAvatar = settingsObj.rightJustifyAvatars; - if (typeof(settingsObj["msgListSort"]) === "string") - { - if (settingsObj.msgListSort.toUpperCase() == "WRITTEN") - this.msgListSort = MSG_LIST_SORT_DATETIME_WRITTEN; - } - if (typeof(settingsObj["convertYStyleMCIAttrsToSync"]) === "boolean") - this.convertYStyleMCIAttrsToSync = settingsObj.convertYStyleMCIAttrsToSync; - if (typeof(settingsObj["prependFowardMsgSubject"]) === "boolean") - this.prependFowardMsgSubject = settingsObj.prependFowardMsgSubject; - if (typeof(settingsObj["enableIndexedModeMsgListCache"]) === "boolean") - this.enableIndexedModeMsgListCache = settingsObj.enableIndexedModeMsgListCache; - if (typeof(settingsObj["themeFilename"]) === "string") - { - // 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 + settingsObj.themeFilename; - if (!file_exists(themeFilename)) - themeFilename = system.ctrl_dir + settingsObj.themeFilename; - if (!file_exists(themeFilename)) - themeFilename = gStartupPath + settingsObj.themeFilename; - } - if (typeof(settingsObj["saveAllHdrsWhenSavingMsgToBBSPC"]) === "boolean") - this.saveAllHdrsWhenSavingMsgToBBSPC = settingsObj.saveAllHdrsWhenSavingMsgToBBSPC; - // User setting defaults - if (typeof(settingsObj.reverseListOrder === "boolean")) - this.userSettings.listMessagesInReverse = settingsObj.reverseListOrder; - if (typeof(settingsObj.useIndexedModeForNewscan) === "boolean") - this.userSettings.useIndexedModeForNewscan = settingsObj.useIndexedModeForNewscan; - if (typeof(settingsObj.newscanOnlyShowNewMsgs) === "boolean") - this.userSettings.newscanOnlyShowNewMsgs = settingsObj.newscanOnlyShowNewMsgs; - if (typeof(settingsObj.indexedModeMenuSnapToFirstWithNew) === "boolean") - this.userSettings.indexedModeMenuSnapToFirstWithNew = settingsObj.indexedModeMenuSnapToFirstWithNew; - if (typeof(settingsObj.promptDelPersonalEmailAfterReply) === "boolean") - this.userSettings.promptDelPersonalEmailAfterReply = settingsObj.promptDelPersonalEmailAfterReply; - if (typeof(settingsObj.displayIndexedModeMenuIfNoNewMessages) === "boolean") - this.userSettings.displayIndexedModeMenuIfNoNewMessages = settingsObj.displayIndexedModeMenuIfNoNewMessages; - } - 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.attributes = "N"; - console.crlf(); - console.print("\x01w\x01hUnable to open the configuration file: \x01y" + this.cfgFilename); - console.crlf(); - console.print("\x01wDefault 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 onlySyncAttrsRegexWholeWord = new RegExp("^[\x01krgybmcw01234567hinq,;\.dtlasz]+$", 'i'); - - var themeFile = new File(themeFilename); - if (themeFile.open("r")) - { - var themeSettingsObj = themeFile.iniGetObject(); - themeFile.close(); - - // Set any color values specified - for (var prop in this.colors) - { - if (themeSettingsObj.hasOwnProperty(prop)) - { - // Trim spaces from the color value - var value = trimSpaces(themeSettingsObj[prop].toString(), true, true, true); - value = value.replace(/\\x01/g, "\x01"); // Replace "\x01" with control character - // If the value doesn't have any control characters, then add the control character - // before attribute characters - if (!/\x01/.test(value)) - value = attrCodeStr(value); - if (onlySyncAttrsRegexWholeWord.test(value)) - this.colors[prop] = value; - } - } - // Set any text strings specified - for (var prop in this.text) - { - if (typeof(themeSettingsObj[prop]) === "string" && themeSettingsObj[prop].length > 0) - { - // Replace any instances of "\x01" with the Synchronet - // attribute control character - this.text[prop] = themeSettingsObj[prop].replace(/\\x01/g, "\x01"); - } - } - // Append the hotkey help line colors with their background color, to ensure that - // the background always gets set for all of them (in case a 'normal' attribute - // appears in any of the colors) - // Message list - this.colors.lightbarMsgListHelpLineGeneralColor += this.colors.lightbarMsgListHelpLineBkgColor; - this.colors.lightbarMsgListHelpLineHotkeyColor += this.colors.lightbarMsgListHelpLineBkgColor; - this.colors.lightbarMsgListHelpLineParenColor += this.colors.lightbarMsgListHelpLineBkgColor; - // Area chooser - this.colors.lightbarAreaChooserHelpLineGeneralColor += this.colors.lightbarAreaChooserHelpLineBkgColor; - this.colors.lightbarAreaChooserHelpLineHotkeyColor += this.colors.lightbarAreaChooserHelpLineBkgColor; - this.colors.lightbarAreaChooserHelpLineParenColor += this.colors.lightbarAreaChooserHelpLineBkgColor; - // Reader - this.colors.enhReaderHelpLineGeneralColor += this.colors.enhReaderHelpLineBkgColor; - this.colors.enhReaderHelpLineHotkeyColor += this.colors.enhReaderHelpLineBkgColor; - this.colors.enhReaderHelpLineParenColor += this.colors.enhReaderHelpLineBkgColor; - // Indexed mode newscan - this.colors.lightbarIndexedModeHelpLineHotkeyColor += this.colors.lightbarIndexedModeHelpLineBkgColor; - this.colors.lightbarIndexedModeHelpLineGeneralColor += this.colors.lightbarIndexedModeHelpLineBkgColor; - this.colors.lightbarIndexedModeHelpLineParenColor += this.colors.lightbarIndexedModeHelpLineBkgColor; - - // 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.attributes = "N"; - console.crlf(); - console.print("\x01w\x01hUnable to open the theme file: \x01y" + themeFilename); - console.crlf(); - console.print("\x01wDefault settings will be used. Please notify the sysop."); - mswait(2000); - } - } -} -// For the DigDistMsgReader class: Reads the user settings file -// -// Parameters: -// pOnlyTwitlist: Optional boolean - Whether or not to only read the user's twitlist. Defaults to false. -function DigDistMsgReader_ReadUserSettingsFile(pOnlyTwitlist) -{ - var onlyTwitList = (typeof(pOnlyTwitlist) === "boolean" ? pOnlyTwitlist : false); - // Open the user's personal twit list file, if it exists - var userTwitlistFile = new File(gUserTwitListFilename); - if (userTwitlistFile.open("r")) - { - while (!userTwitlistFile.eof) - { - // Read the next line from the config file. - var fileLine = userTwitlistFile.readln(2048); - - // fileLine should be a string, but I've seen some cases - // where for some reason it isn't. If it's not a string, - // then continue onto the next line. - 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; - - // In case there are any commas, split on commas. Add all names to the user's twitlist - var names = fileLine.split(","); - for (var i = 0; i < names.length; ++i) - { - var twitListNameEntry = names[i].trim().toLowerCase(); // Lowercase for case-insensitive comparisons - if (twitListNameEntry.length > 0) - this.userSettings.twitList.push(twitListNameEntry); - } - } - userTwitlistFile.close(); - } - - if (!onlyTwitList) - { - // Open and read the user settings file, if it exists - var userSettingsFile = new File(gUserSettingsFilename); - if (userSettingsFile.open("r")) - { - // Variables in this.userSettings are initialized in the DigDistMsgReader constructor. Then, default user - // settings are set when reading DDMsgReader.cfg, which is read before the user settings file. So for each - // user setting (except for twitlist), try to read it from the user settings file, but heave the default be - // whatever it's currently set to. - for (var settingName in this.userSettings) - { - if (settingName == "twitList") continue; - this.userSettings[settingName] = userSettingsFile.iniGetValue("BEHAVIOR", settingName, this.userSettings[settingName]); - } - - userSettingsFile.close(); - } - } -} - -// For the DigDistMessageReader class: Writes the user settings file. -// -// Return value: Boolean - Whether or not the write succeeded -function DigDistMsgReader_WriteUserSettingsFile() -{ - var writeSucceeded = false; - // Open the user settings file, if it exists - var userSettingsFile = new File(gUserSettingsFilename); - if (userSettingsFile.open(userSettingsFile.exists ? "r+" : "w+")) - { - // Variables in this.userSettings are initialized in the DigDistMsgReader constructor. For each - // user setting (except for twitlist), save the setting in the user's settings file. The user's - // twit list is an array that is saved to a separate file. - for (var settingName in this.userSettings) - { - if (settingName == "twitList") continue; - userSettingsFile.iniSetValue("BEHAVIOR", settingName, this.userSettings[settingName]); - } - userSettingsFile.close(); - writeSucceeded = true; - } - return writeSucceeded; -} - -// 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 = { - userCannotEdit: false, - userConfirmed: false, - msgEdited: false, - newMsgIdx: -1 - }; - - // Open the sub-board - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - { - console.print("\x01n\x01h\x01wCan't open the sub-board\x01n"); - console.crlf(); - console.pause(); - return returnObj; - } - - // Only let the user edit the message if they're a sysop or - // if they wrote the message. - var msgHeader = this.GetMsgHdrByIdx(pMsgIndex, false, msgbase); - if (msgHeader == null) - { - console.print("\x01n\x01h\x01wInvalid message header\x01n"); - console.crlf(); - console.pause(); - return returnObj; - } - if (!user.is_sysop && (msgHeader.from != user.name) && (msgHeader.from != user.alias) && (msgHeader.from != user.handle)) - { - console.print("\x01n\x01h\x01wCannot edit message #\x01y" + +(pMsgIndex+1) + - " \x01wbecause it's not yours or you're not a sysop.\x01n"); - console.crlf(); - console.pause(); - returnObj.userCannotEdit = true; - return returnObj; - } - - // Confirm the action with the user (default to no). - returnObj.userConfirmed = !console.noyes(replaceAtCodesInStr(format(this.text.msgEditConfirmText, +(pMsgIndex+1)))); - if (!returnObj.userConfirmed) - { - msgbase.close(); - return returnObj; - } - - // Make use of bbs.edit_msg() if the function exists (it was added in - // Synchronet 3.18c). Otherwise, edit the old way. - if (typeof(bbs.edit_msg) === "function") - { - if (!bbs.edit_msg(msgHeader)) - { - var grpIdx = msg_area.sub[this.subBoardCode].grp_index; - var areaDesc = msg_area.grp_list[grpIdx].description + " - " + msg_area.sub[this.subBoardCode].description; - var logMsg = user.alias + " was unable to edit message number " + msgHeader.number + " in " + areaDesc; - log(LOG_ERROR, logMsg); - bbs.log_str(logMsg); - } - } - else - this.EditExistingMessageOldWay(msgbase, msgHeader, pMsgIndex); - - msgbase.close(); - - return returnObj; -} -// Helper for DigDistMsgReader_EditExistingMsg(): Edits an existing message by writing it -// to a temporary file, having the user edit that, and saving it as a new message. -// This was done before the bbs.edit_msg() function existed (it was added in Synchronet -// 3.18c). -// -// Parameters: -// pMsgbase: The MessageBase object. Assumed to be open. -// pOrigMsgHdr: The header of the original message -// pMsgIndex: The index of the message to edit -function DigDistMsgReader_EditExistingMessageOldWay(pMsgbase, pOrigMsgHdr, pMsgIndex) -{ - // Dump the message body to a temporary file in the node dir - //var originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true); - var originalMsgBody; - var tmpMsgHdr = this.GetMsgHdrByIdx(pMsgIndex, false, pMsgbase); - if (tmpMsgHdr == null) - originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true); - else - originalMsgBody = pMsgbase.get_msg_body(false, tmpMsgHdr.number, false, false, true, true); - 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 = pOrigMsgHdr.to; - bbs.msg_to_ext = pOrigMsgHdr.to_ext; - bbs.msg_subject = pOrigMsgHdr.subject; - bbs.msg_offset = pOrigMsgHdr.offset; - bbs.msg_number = pOrigMsgHdr.number; - - // Let the user edit the temporary file - console.editfile(tempFilename); - // Load the temp file back into msgBodyColor and have pMsgbase - // 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: pOrigMsgHdr.to, to_ext: pOrigMsgHdr.to_ext, from: pOrigMsgHdr.from, - from_ext: pOrigMsgHdr.from_ext, attr: pOrigMsgHdr.attr, - subject: pOrigMsgHdr.subject }; - var savedNewMsg = pMsgbase.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 = pMsgbase.total_msgs - 1; - var message = "\x01n\x01cThe edited message has been saved as a new message."; - if (pMsgbase.remove_msg(true, pMsgIndex)) - message += " The original has been\r\nmarked for deletion."; - else - message += " \x01h\x01yHowever, the original\r\ncould not be marked for deletion."; - message += "\r\n\x01p"; - console.print(message); - } - else - console.print("\r\n\x01n\x01h\x01yError: \x01wFailed to save the new message\r\n\x01p"); - } - } - else - { - console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to read the temporary file\r\n"); - console.print("Filename: \x01b" + tempFilename + "\r\n"); - console.pause(); - } - } - else - { - console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to write to temporary file\r\n"); - console.print("Filename: \x01b" + tempFilename + "\r\n"); - console.pause(); - } - } - else - { - console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to open a temporary file for writing\r\n"); - console.print("Filename: \x01b" + tempFilename + "\r\n"); - console.pause(); - } - // Delete the temporary file from disk. - tmpFile.remove(); -} - -// 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() -{ - // Deleting messages is allowed if the user is the sysop or reading personal email. - // If not, check the sub-board configuration. - var canDelete = user.is_sysop || this.readingPersonalEmail; - if (!canDelete) - canDelete = Boolean(msg_area.sub[this.subBoardCode].settings & 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() -{ - // Sysops can delete the last message by default. If not, check the sub-board configuration. - var canDelete = user.is_sysop; - if (!canDelete && !this.readingPersonalEmail) - canDelete = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_DELLAST); - return canDelete; -} -// For the DigDistMsgReader Class: Returns whether or not the user can edit -// messages. -function DigDistMsgReader_CanEdit() -{ - // Sysops can edit by default. If not, check the sub-board configuration. - var canEdit = user.is_sysop; - if (!canEdit && !this.readingPersonalEmail) - canEdit = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_EDIT); - return canEdit; -} -// For the DigDistMsgReader Class: Returns whether or not message quoting -// is enabled. -function DigDistMsgReader_CanQuote() -{ - // Sysops and users reading personal email can quote by default. - // If not, check the sub-board configuration. - var canQuote = user.is_sysop || this.readingPersonalEmail; - if (!canQuote) - canQuote = Boolean(msg_area.sub[this.subBoardCode].settings & 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, false, 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("\x01n\x01w" + charStr(HORIZONTAL_DOUBLE, 78)); - console.crlf(); - var horizSingleFive = charStr(HORIZONTAL_SINGLE, 5); - console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr["from"].substr(0, console.screen_columns-12)); - console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cTo \x01w\x01h: \x01b" + pMsgHdr["to"].substr(0, console.screen_columns-12)); - console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr["subject"].substr(0, console.screen_columns-12)); - console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12)); - console.crlf(); - console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + 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 -// msgbase.total_msgs. -// -// Parameters: -// pMsgbase: Optional - A MessageBase object -// -// Return value: The number of messages -function DigDistMsgReader_NumMessages(pMsgbase) -{ - var numMsgs = 0; - if (this.SearchingAndResultObjsDefinedForCurSub()) - numMsgs = this.msgSearchHdrs[this.subBoardCode].indexed.length; - else if (this.hdrsForCurrentSubBoard.length > 0) - numMsgs = this.hdrsForCurrentSubBoard.length; - else - { - var closeMsgbaseInThisFunc = false; - var msgbase = null; - if (pMsgbase != null && typeof(pMsgbase) === "object") - msgbase = pMsgbase; - else - { - closeMsgbaseInThisFunc = true; - msgbase = new MsgBase(this.subBoardCode); - msgbase.open(); - } - if (msgbase.is_open) - { - //numMsgs = msgbase.total_msgs; - // Count the number of readable messages in the messagebase (i.e., - // messages that are not deleted, unvalidated, or null headers) - numMsgs = 0; - var indexRecords = msgbase.get_index(); - if (indexRecords != null) - { - for (var i = 0; i < indexRecords.length; ++i) - { - if (isReadableMsgHdr(indexRecords[i], this.subBoardCode)) - ++numMsgs; - } - } - if (closeMsgbaseInThisFunc) - msgbase.close(); - } - } - - 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 != null && (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 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 = 0; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - highestMessageNum = msgbase.total_msgs; - msgbase.close(); - } - 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 - // msgbase. - var msgNumIsValid = false; - if (this.SearchingAndResultObjsDefinedForCurSub()) - msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length)); - else - { - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs)); - msgbase.close(); - } - } - return msgNumIsValid; -} - -// For the DigDistMsgReader class: Returns a message header by index. Will look -// in this.msgSearchHdrs if it's not empty, then in this.hdrsForCurrentSubBoard -// if it's not empty, then from msgbase. -// -// Parameters: -// pMsgIdx: The message index (0-based) -// pExpandFields: Whether or not to expand fields. Defaults to false. -// pMsgbase: Optional - An open MsgBase object. If not passed, the sub-board coould be opened in this method. -function DigDistMsgReader_GetMsgHdrByIdx(pMsgIdx, pExpandFields, pMsgbase) -{ - var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false); - - 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)) - { - if (expandFields) - msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx]; - else - msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx].number, expandFields); - } - } - else if (this.hdrsForCurrentSubBoard.length > 0) - { - if ((pMsgIdx >= 0) && (pMsgIdx < this.hdrsForCurrentSubBoard.length)) - { - if (expandFields) - msgHdr = this.hdrsForCurrentSubBoard[pMsgIdx]; - else - msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.hdrsForCurrentSubBoard[pMsgIdx].number, expandFields); - } - } - else - msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, true, pMsgIdx, pExpandFields); - 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, then in -// this.hdrsForCurrentSubBoard if it's not empty, then from msgbase. -// -// Parameters: -// pMsgNum: The message number (1-based) -// pExpandFields: Whether or not to expand fields. Defaults to false. -// -// Return value: The message header for the message number, or null on error -function DigDistMsgReader_GetMsgHdrByMsgNum(pMsgNum, pExpandFields) -{ - 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 (this.hdrsForCurrentSubBoard.length > 0) - { - if ((pMsgNum > 0) && (pMsgNum <= this.hdrsForCurrentSubBoard.length)) - msgHdr = this.hdrsForCurrentSubBoard.length[pMsgNum-1]; - } - else - { - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs)) - { - var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false); - msgHdr = msgbase.get_msg_header(true, pMsgNum-1, expandFields); - } - msgbase.close(); - } - } - 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 -// pExpandFields: Whether or not to expand fields. Defaults to false. -// pGetVoteInfo: Whether or not to get voting information. Defaults to false. -// -// Return value: The message header for the message number, or null on error -function DigDistMsgReader_GetMsgHdrByAbsoluteNum(pMsgNum, pExpandFields, pGetVoteInfo) -{ - var msgHdr = null; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : false); - var getVoteInfo = (typeof(pGetVoteInfo) === "boolean" ? pGetVoteInfo : false); - msgHdr = msgbase.get_msg_header(false, pMsgNum, expandFields, getVoteInfo); - msgbase.close(); - } - return msgHdr; -} -function DumpMsgHdr(pSubCode, pMsgNum, pExpandFields, pGetVoteInfo) -{ - var hdrLines = []; - var msgHdr = null; - var msgbase = new MsgBase(pSubCode); - if (msgbase.open()) - { - var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : false); - var getVoteInfo = (typeof(pGetVoteInfo) === "boolean" ? pGetVoteInfo : false); - msgHdr = msgbase.get_msg_header(false, pMsgNum, expandFields, getVoteInfo); - if (msgHdr != null) - hdrLines = msgbase.dump_msg_header(msgHdr); - msgbase.close(); - } - return hdrLines; -} - -// 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.subBoardCode, pMsgNum); - //if (this.doingNewscan && this.userSettings.newscanOnlyShowNewMsgs) - - // Check this.msgNumToIdxMap and/or this.hdrsForCurrentSubBoard if - // they're populated and return the index from that. Otherwise, - // check the messagebase directly. - var msgIdx = -1; - if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum)) - msgIdx = this.msgNumToIdxMap[pMsgNum]; - else if (this.hdrsForCurrentSubBoard.length > 0) - { - for (var i = 0; i < this.hdrsForCurrentSubBoard.length; ++i) - { - if (this.hdrsForCurrentSubBoard[i].number == pMsgNum) - { - msgIdx = this.hdrsForCurrentSubBoard[i].number; - break; - } - } - } - else - msgIdx = absMsgNumToIdx(this.subBoardCode, pMsgNum); - return msgIdx; -} - -// 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) -{ - var msgIdx = -1; - // Check this.hdrsForCurrentSubBoard if it's populated and return - // the message number from the header at the given index if exists. - // therwise, check the messagebase directly. - if (this.hdrsForCurrentSubBoard.length > 0) - { - if (pMsgIdx >= 0 && pMsgIdx < this.hdrsForCurrentSubBoard.length) - msgIdx = this.hdrsForCurrentSubBoard[pMsgIdx].number; - } - else - { - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - msgIdx = idxToAbsMsgNum(msgbase, pMsgIdx); - msgbase.close(); - } - } - return msgIdx; -} - -// 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("\x01n"); - else - { - if (lastErrorLen > promptTextLen) - { - console.attributes = "N"; - console.gotoxy(pCurPos.x + promptTextLen, pCurPos.y); - var clearLen = lastErrorLen - promptTextLen; - printf("%-*s", clearLen, ""); - } - } - console.gotoxy(pCurPos.x + promptTextLen, pCurPos.y); - } - msgNum = console.getnum(this.NumMessages()); // this.HighestMessageNum() - // If the message number is invalid, then output an error message. - if (msgNum != 0) - { - if (!this.IsValidMessageNum(msgNum) && msgNum != -1) // msgNum would be -1 if the user pressed Q to quit - { - // 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, - "\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, msgNum)) + "\x01n", - pErrorPauseTimeMS, "\x01n", true); - console.gotoxy(pCurPos); - } - else - { - console.print("\x01n\x01w\x01h" + msgNum + " \x01yis 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. -// pBBSLocalTimeZone: Optional boolean - Whether or not pDateTimeStr is in the BBS's -// local time zone. Defaults to false. -// 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, - pBBSLocalTimeZone, pAllowCLS) -{ - var textLine = pTextLine; - var dateTimeStr = ""; - var useBBSLocalTimeZone = false; - if (typeof(pDateTimeStr) == "string") - { - dateTimeStr = pDateTimeStr; - if (typeof(pBBSLocalTimeZone) == "boolean") - useBBSLocalTimeZone = pBBSLocalTimeZone; - } - else - dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", pMsgHdr.when_written_date); - // 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_AND_FROM_NET", "@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, useBBSLocalTimeZone, - 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, useBBSLocalTimeZone, mainMsgAttrStr, 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")) - { - if ((typeof(file_area.lib_list[bbs.curlib]) == "object") && (typeof(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir]) == "object")) - { - 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 msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHdr); - // This list has some custom @-codes: - var newTxtLine = textLine.replace(/@MSG_SUBJECT@/gi, pMsgHdr["subject"]) - .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, (useBBSLocalTimeZone ? system.zonestr(system.timezone) : 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(/@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) - .replace(/@MSG_UPVOTES@/gi, msgVoteInfo.upvotes) - .replace(/@MSG_DOWNVOTES@/gi,msgVoteInfo.downvotes) - .replace(/@MSG_SCORE@/gi, msgVoteInfo.voteScore); - // If the user is not the sysop and the message was posted anonymously, - // then replace the from name @-codes with "Anonymous". Otherwise, - // replace the from name @-codes with the actual from name. - if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - { - newTxtLine = newTxtLine.replace(/@MSG_FROM@/gi, "Anonymous") - .replace(/@MSG_FROM_AND_FROM_NET@/gi, "Anonymous") - .replace(/@MSG_FROM_EXT@/gi, "Anonymous"); - } - else - { - newTxtLine = newTxtLine.replace(/@MSG_FROM@/gi, pMsgHdr["from"]) - .replace(/@MSG_FROM_AND_FROM_NET@/gi, pMsgHdr["from"] + (typeof(pMsgHdr["from_net_addr"]) == "string" ? " (" + pMsgHdr["from_net_addr"] + ")" : "")) - .replace(/@MSG_FROM_EXT@/gi, (typeof(pMsgHdr["from_ext"]) == "string" ? pMsgHdr["from_ext"] : "")); - } - if (!pAllowCLS) - newTxtLine = newTxtLine.replace(/@CLS@/gi, ""); - newTxtLine = replaceAtCodesInStr(newTxtLine); - 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 -// pUseBBSLocalTimeZone: Boolean - Whether or not pDateTimeStr is in the BBS's local time zone. -// 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, pUseBBSLocalTimeZone, 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_AND_FROM_NET") > -1) - { - // If the user is not the sysop and the message was posted anonymously, - // then replace the from name @-codes with "Anonymous". Otherwise, - // replace the from name @-codes with the actual from name. - if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - replacementTxt = "Anonymous".substr(0, pSpecifiedLen); - else - { - var fromWithNet = pMsgHdr["from"] + (typeof(pMsgHdr["from_net_addr"]) == "string" ? " (" + pMsgHdr["from_net_addr"] + ")" : ""); - replacementTxt = fromWithNet.substr(0, pSpecifiedLen); - } - } - else if (pAtCodeStr.indexOf("@MSG_FROM") > -1) - { - // If the user is not the sysop and the message was posted anonymously, - // then replace the from name @-codes with "Anonymous". Otherwise, - // replace the from name @-codes with the actual from name. - if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - replacementTxt = "Anonymous".substr(0, pSpecifiedLen); - else - replacementTxt = pMsgHdr["from"].substr(0, pSpecifiedLen); - } - else if (pAtCodeStr.indexOf("@MSG_FROM_EXT") > -1) - { - // If the user is not the sysop and the message was posted anonymously, - // then replace the from name @-codes with "Anonymous". Otherwise, - // replace the from name @-codes with the actual from name. - if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS)) - replacementTxt = "Anonymous".substr(0, pSpecifiedLen); - else - 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) - { - if (pUseBBSLocalTimeZone) - replacementTxt = system.zonestr(system.timezone).substr(0, pSpecifiedLen); - else - 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 readable (not marked as deleted or the user can read deleted messages, -// etc.). If none is found, the return value will be -1. -// -// 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). -// pUseCachedHdrs: Optional boolean - Whether or not to use the cached message headers -// (which are filtered with deleted messages removed, etc). Defaults to true. -// -// Return value: The index of the next message prior/later that is not marked -// as deleted, or -1 if none is found. -function DigDistMsgReader_FindNextReadableMsgIdx(pOffset, pForward, pUseCachedHdrs) -{ - // Sanity checking for the parameters & other things - if (typeof(pOffset) != "number") - return -1; - var searchForward = (typeof(pForward) == "boolean" ? pForward : true); - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - return -1; - - var useCachedHdrs = (typeof(pUseCachedHdrs) === "boolean" ? pUseCachedHdrs : true); - var newMsgIdx = -1; - if (searchForward) - { - // Search forward for a message that isn't marked for deletion (and check if the user - // can read deleted messages) - if (useCachedHdrs) - { - var numOfMessages = this.NumMessages(msgbase); - if (pOffset < numOfMessages - 1) - { - var hdrIsBogus; - for (var messageIdx = pOffset+1; (messageIdx < numOfMessages) && (newMsgIdx == -1); ++messageIdx) - { - var nextMsgHdr = this.GetMsgHdrByIdx(messageIdx, false, msgbase); - if (nextMsgHdr != null && !isVoteHdr(nextMsgHdr)) - { - var canRead = true; - if ((nextMsgHdr.attr & MSG_DELETE) == MSG_DELETE) - canRead = canViewDeletedMsgs(); - if (canRead) - newMsgIdx = messageIdx; - } - } - } - } - else - { - // TODO - } - } - else - { - // Search backward for a message that isn't marked for deletion (and check if the user - // can read deleted messages) - if (pOffset > 0) - { - if (useCachedHdrs) - { - var hdrIsBogus; - for (var messageIdx = pOffset-1; (messageIdx >= 0) && (newMsgIdx == -1); --messageIdx) - { - var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx, false, msgbase); - if (prevMsgHdr != null && !isVoteHdr(prevMsgHdr)) - { - var canRead = true; - if ((prevMsgHdr.attr & MSG_DELETE) == MSG_DELETE) - canRead = canViewDeletedMsgs(); - if (canRead) - newMsgIdx = messageIdx; - } - } - } - else - { - // TODO - } - } - } - msgbase.close(); - 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-read message in the sub-board -// lastReadMsgNum: The message number of the last-read message in the sub-board -// -function DigDistMsgReader_ChangeSubBoard(pNewSubBoardCode) -{ - var retObj = { - succeeded: false, - lastReadMsgIdx: 0, - lastReadMsgNum: 0 - }; - - var newSubBoardCode = bbs.cursub_code; - if (typeof(pNewSubBoardCode) == "string") - newSubBoardCode = pNewSubBoardCode; - if (typeof(msg_area.sub[newSubBoardCode]) != "object") - { - console.clear("\x01n"); - console.print("\x01n\x01h\x01y* \x01wSomething 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) - { - this.setSubBoardCode(newSubBoardCode); - this.PopulateHdrsForCurrentSubBoard(); - } - - // 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. - var lastReadMsgInfo = this.GetLastReadMsgIdxAndNum(); - retObj.lastReadMsgIdx = lastReadMsgInfo.lastReadMsgIdx; - retObj.lastReadMsgNum = lastReadMsgInfo.lastReadMsgNum; - 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("\x01n"); - 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. This needs to be a header with -// fields expanded. -// 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 -function DigDistMsgReader_ReplyToMsg(pMsgHdr, pMsgText, pPrivate, pMsgIdx) -{ - var retObj = { - postSucceeded: false, - msgWasDeleted: false - }; - - 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 (Boolean(pMsgHdr.attr & MSG_NOREPLY)) - { - console.crlf(); - console.print("\x01n\x01y\x01hReplies 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("\x01n\x01y\x01hYou 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. - // TODO: Handle things when reading another user's email (for the sysop) - "mail" as a sub-board code might - // not work - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var quoteFile = null; - if (this.CanQuote()) - { - // Get the user's setting for whether or not to wrap quote lines (and how long) from - // their external editor settings - var editorQuoteCfg = getExternalEditorQuoteWrapCfgFromSCFG(user.editor); - // Write the message text to the quotes file - quoteFile = new File(system.node_dir + "QUOTES.TXT"); - if (quoteFile.open("w")) - { - var msgNum = (typeof(pMsgIdx) === "number" ? pMsgIdx+1 : null); - var msgText = ""; - if (typeof(pMsgText) == "string") - msgText = pMsgText; - else - msgText = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true); - if (editorQuoteCfg.quoteWrapEnabled && editorQuoteCfg.quoteWrapCols > 0) - msgText = word_wrap(msgText, editorQuoteCfg.quoteWrapCols, msgText.length, false); - quoteFile.write(msgText); - - quoteFile.close(); - // Let the user quote in the reply - replyMode |= WM_QUOTE; - } - } - } - - // Strip any control characters that the subject line might have - pMsgHdr.subject = strip_ctrl(pMsgHdr.subject); - - // 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; - // If the user successfully saved the message and the message wasn't deleted, - // then apply the 'replied' attribute to the message Header - if (privReplRetObj.sendSucceeded && !privReplRetObj.msgWasDeleted) - { - var saveRetObj = applyAttrsInMsgHdrInMessagbase(this.subBoardCode, pMsgHdr.number, MSG_REPLIED); - if (saveRetObj.saveSucceeded) - this.RefreshHdrInSavedArrays(pMsgIdx, MSG_REPLIED, true); - } - } - 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(msgbase.last_msg.toString()); // Highest message # - msgBaseInfoFile.writeln(this.NumMessages(msgbase).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) - 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 = 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. - // 2016-08-26: Updated to not close the messagebase because a private - // reply on a networked sub-board needs to be able to get a message - // header with fields expanded. - 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 - // TODO: Saw this error message once on the next line: - // Error: Error -300 adding RFC822MSGID field to message header - retObj.postSucceeded = bbs.post_msg(this.subBoardCode, replyMode, pMsgHdr); - console.pause(); - } - msgBaseInfoFile.remove(); - var msgbaseReOpened = 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. - if (retObj.postSucceeded && msgbaseReOpened && (msgbase.total_msgs > numMessagesBefore)) - { - // If doing a newscan and the user setting for only showing new messages during a newscan - // is enabled, then get the last message header (which should be the message the user - // just posted) and add it to the cached array of message headers - if (this.doingNewscan && this.userSettings.newscanOnlyShowNewMsgs) - { - var lastMsgHdr = msgbase.get_msg_header(false, msgbase.last_msg); - if (msgIsFromUser(lastMsgHdr)) - { - this.hdrsForCurrentSubBoard.push(lastMsgHdr); - this.msgNumToIdxMap[lastMsgHdr.number] = this.hdrsForCurrentSubBoard.length - 1; - } - } - // If doing a search and the search headers has the current sub-board, then add the posted - // message to the search headers - else if (this.SearchTypePopulatesSearchResults() && this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) - { - if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)) - this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser); - else - { - var msgHeaders = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser, numMessagesBefore, 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; - } - } - } - // If we have cached message headers, add the user's just-posted message - else if (this.hdrsForCurrentSubBoard.length > 0) - { - //this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(msgbase.get_all_msg_headers(true), true); - var lastMsgHdr = msgbase.get_msg_header(false, msgbase.last_msg); - if (msgIsFromUser(lastMsgHdr)) - { - this.hdrsForCurrentSubBoard.push(lastMsgHdr); - this.msgNumToIdxMap[lastMsgHdr.number] = this.hdrsForCurrentSubBoard.length - 1; - } - } - } - } - - // Delete the quote file - if (quoteFile != null) - quoteFile.remove(); - - msgbase.close(); - - 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. This needs to be a header with expanded fields. -// 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 = { - sendSucceeded: true, - msgWasDeleted: false - }; - - if (pMsgHdr == null) - { - retObj.sendSucceeded = false; - return retObj; - } - - // Set up the initial reply mode bits - var replyMode = WM_NONE; - if (typeof(pReplyMode) == "number") - replyMode |= pReplyMode; - - // If the message is a networked message, then try to address the message - // to the network address. - var couldNotDetermineNetAddr = true; - var wasNetMailOrigin = false; - if ((typeof(pMsgHdr.from_net_type) != "undefined") && (pMsgHdr.from_net_type != NET_NONE)) - { - wasNetMailOrigin = true; - if ((typeof(pMsgHdr.from_net_addr) == "string") && (pMsgHdr.from_net_addr.length > 0)) - { - couldNotDetermineNetAddr = false; - // 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 (typeof(pMsgHdr.from_net_addr) === "string" && pMsgHdr.from_net_addr.length > 0) - { - 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); - emailAddr = console.getstr(emailAddr, 60, K_LINE|K_EDIT); - if ((typeof(emailAddr) == "string") && (emailAddr.length > 0)) - { - replyMode |= WM_NETMAIL; - retObj.sendSucceeded = bbs.netmail(emailAddr, replyMode, null, pMsgHdr); - console.pause(); - } - else - { - retObj.sendSucceeded = false; - console.putmsg(bbs.text(Aborted), P_SAVEATR); - console.pause(); - } - } - } - // If we could not determine the network mail address, we may need to try to look up the user to - // reply locally. - if (couldNotDetermineNetAddr) - { - // Most likely replying to a local user - replyMode |= WM_EMAIL; - // Look up the user number of the "from" user name in the message header - var userNumber = findUserNumWithName(pMsgHdr.from); // Used to use 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, null, null, pMsgHdr); - console.pause(); - } - else - { - // If the 'from' username is blank (which can be the case if a guest sent the email), then - // ask the user where or to whom they want to send the message - if (pMsgHdr.from.length == 0) - { - console.attributes = "NC"; - console.crlf(); - console.print("Sender is unknown. Enter user name/number/email/netmail address\x01h:\x01n"); - console.crlf(); - var msgDest = console.getstr(console.screen_columns - 1, K_LINE); - if (msgDest != "") - { - var recipientMatched = true; - var sendViaNetmail = false; - // See if the user entered a netmail address - if (gEmailRegex.test(msgDest) || gFTNEmailRegex.test(msgDest)) - sendViaNetmail = true; - // Check for a valid user number - else if (/^[0-9]+/.test(msgDest)) - { - if (system.username(+msgDest) != "") - userNumber = +msgDest; - else - recipientMatched = false; - } - // Match local user by name/alias - else - { - userNumber = findUserNumWithName(msgDest); - if (userNumber <= 0) - recipientMatched = false; - } - // If no recipient was matched, then output an error. Otherwise, do a reply. - if (!recipientMatched) - { - retObj.sendSucceeded = false; - console.crlf(); - var errorMsg = "\x01n\x01h\x01yThe recipient (\x01w" + msgDest + "\x01y) was not found"; - if (wasNetMailOrigin) - errorMsg += " and no network address was found for this message"; - errorMsg += "\x01n"; - console.print(errorMsg); - console.crlf(); - console.pause(); - } - else if (sendViaNetmail) - { - replyMode |= WM_NETMAIL; - retObj.sendSucceeded = bbs.netmail(msgDest, replyMode, null, pMsgHdr); - console.pause(); - } - else - { - console.crlf(); - retObj.sendSucceeded = bbs.email(userNumber, replyMode, null, null, pMsgHdr); - console.pause(); - } - } - else - { - console.attributes = "N"; - console.print("Canceled"); - console.crlf(); - } - } - else - { - retObj.sendSucceeded = false; - console.crlf(); - var errorMsg = "\x01n\x01h\x01yThe recipient (\x01w" + pMsgHdr.from + "\x01y) was not found"; - if (wasNetMailOrigin) - errorMsg += " and no network address was found for this message"; - errorMsg += "\x01n"; - console.print(errorMsg); - console.crlf(); - console.pause(); - } - } - } - - // If the user replied to a personal email, and the user setting to prompt - // to delete the message after replying is enabled, 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") && this.userSettings.promptDelPersonalEmailAfterReply) - { - // 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.PromptAndDeleteOrUndeleteMessage(pMsgIdx, null, true, 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("\x01n"); - - 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("\x01n\x01cCurrently reading \x01g" + 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) - { - var numOfMessages = 0; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - numOfMessages = msgbase.total_msgs; - msgbase.close(); - } - console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board."); - 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("\x01n\x01c"); - if (this.readingPersonalEmail) - console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + "."); - else - { - if (numSearchResults == 1) - console.print("There is \x01g1 \x01csearch result."); - else - console.print("There are \x01g" + numSearchResults + " \x01csearch results."); - } - console.crlf(); - } - - // Display the enhanced reader keys - console.crlf(); - console.print("\x01n\x01cEnhanced reader mode keys"); - console.crlf(); - console.print("\x01h\x01k"); - for (var i = 0; i < 25; ++i) - console.print(HORIZONTAL_SINGLE); - console.crlf(); - var keyHelpLines = ["\x01h\x01cDown\x01g/\x01cup arrow \x01g: \x01n\x01cScroll down\x01g/\x01cup in the message", - "\x01h\x01cLeft\x01g/\x01cright arrow \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message", - "\x01h\x01cEnter \x01g: \x01n\x01cGo to the next message", - "\x01h\x01cPageUp\x01g/\x01cPageDown \x01g: \x01n\x01cScroll up\x01g/\x01cdown a page in the message", - "\x01h\x01cHOME \x01g: \x01n\x01cGo to the top of the message", - "\x01h\x01cEND \x01g: \x01n\x01cGo to the bottom of the message"]; - keyHelpLines.push("\x01h\x01cCtrl-U \x01g: \x01n\x01cChange your user settings"); - if (user.is_sysop) - { - keyHelpLines.push("\x01h\x01cDEL \x01g: \x01n\x01cDelete the current message"); - keyHelpLines.push("\x01h\x01cCtrl-S \x01g: \x01n\x01cSave the message (to the BBS machine)"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.validateMsg + " \x01g: \x01n\x01cValidate the message"); - keyHelpLines.push("\x01h\x01cCtrl-O \x01g: \x01n\x01cShow operator menu"); - keyHelpLines.push("\x01h\x01cX \x01g: \x01n\x01cShow message hex dump"); - keyHelpLines.push("\x01h\x01cCtrl-X \x01g: \x01n\x01cSave message hex dump to a file"); - var quickValUserLine = "\x01h\x01cCtrl-Q \x01g: \x01n\x01cQuick-validate user (must be local)"; - if (this.quickUserValSetIndex >= 0 && this.quickUserValSetIndex < 10) - quickValUserLine += "; Set index: " + this.quickUserValSetIndex; - keyHelpLines.push(quickValUserLine); - } - else if (this.CanDelete() || this.CanDeleteLastMsg()) - keyHelpLines.push("\x01h\x01cDEL \x01g: \x01n\x01cDelete the current message (if it's yours)"); - if (displayDLAttachmentsOpt) - keyHelpLines.push("\x01h\x01cCtrl-A \x01g: \x01n\x01cDownload 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 = "\x01h\x01c" + this.enhReaderKeys.prevMsgByThreadID + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByThreadID + " \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message in the thread"; - /* - if (system.version_num >= 31600) - threadIDLine += " (thread ID)"; - */ - keyHelpLines.push(threadIDLine); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByTitle + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByTitle + " \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by title (subject)"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByAuthor + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByAuthor + " \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by author"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByToUser + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByToUser + " \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by 'To user'"); - } - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.firstMsg + " \x01n\x01cor \x01h" + this.enhReaderKeys.lastMsg + " \x01g: \x01n\x01cGo to the first\x01g/\x01clast message in the sub-board"); - if (displayChgAreaOpt) - { - if (this.doingMultiSubBoardScan) - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.nextSubBoard + " \x01g: \x01n\x01cGo to the next message sub-board"); - else - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevSubBoard + "\x01n\x01c or \x01h" + this.enhReaderKeys.nextSubBoard + " \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message sub-board"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.chgMsgArea + " \x01g: \x01n\x01cChange to a different message sub-board"); - } - else if (this.doingMultiSubBoardScan) - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.nextSubBoard + " \x01g: \x01n\x01cGo to the next message sub-board"); - if (user.is_sysop) - { - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.editMsg + " \x01g: \x01n\x01cEdit the current message"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.userEdit + " \x01g: \x01n\x01cEdit the user who wrote the message"); - } - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showMsgList + " \x01g: \x01n\x01cList messages in the current sub-board"); - if (user.is_sysop) - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showHdrInfo + " \x01n\x01cor \x01h" + this.enhReaderKeys.showKludgeLines + " \x01g: \x01n\x01cDisplay extended header info\x01g/\x01ckludge lines for the message"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.reply + " \x01g: \x01n\x01cReply to the current message"); - if (!this.readingPersonalEmail) - { - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.privateReply + " \x01g: \x01n\x01cPrivately reply to the current message (via email/NetMail)"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.postMsg + " \x01g: \x01n\x01cPost a message on the sub-board"); - } - keyHelpLines.push("\x01h\x01cNumber \x01g: \x01n\x01cGo to a specific message by number"); - keyHelpLines.push("\x01h\x01cSpacebar \x01g: \x01n\x01cSelect message (for batch delete, etc.)"); - keyHelpLines.push(" \x01n\x01cFor batch delete, open the message list and use CTRL-D."); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.forwardMsg + " \x01g: \x01n\x01cForward the message to user/email"); - if (typeof((new MsgBase(this.subBoardCode)).vote_msg) === "function") - { - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.vote + " \x01g: \x01n\x01cVote on the message"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.closePoll + " \x01g: \x01n\x01cClose a poll"); - } - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showVotes + " \x01g: \x01n\x01cShow vote (tally) stats for the message"); - keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.quit + " \x01g: \x01n\x01cQuit back to the BBS"); - if (this.indexedMode) - keyHelpLines.push(" \x01n\x01cCurrently in indexed mode; quitting will quit back to the index list."); - for (var idx = 0; idx < keyHelpLines.length; ++idx) - { - console.print("\x01n" + 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("\x01n" + this.pausePromptText); - //getKeyWithESCChars(K_NOSPIN|K_NOCRLF|K_NOECHO); - // I'm not sure the above is needed anymore. Should be able to use - // console.pause(), which easily supports custom pause scripts being loaded. - console.pause(); - - console.aborted = false; -} - -// 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; - // For the set of enhanced header lines, choose the regular set or the set with - // the highlighted 'to' user, dependin on whether the message was written to the - // logged-in (reading) user. - var enhMsgHdrLines = (userHandleAliasNameMatch(pMsgHdr.to) ? this.enhMsgHeaderLinesToReadingUser : this.enhMsgHeaderLines); - if (enhMsgHdrLines == null) - return; - if ((enhMsgHdrLines.length == 0) || (this.enhMsgHeaderWidth == 0)) - return; - - // Create a formatted date & time string. Adjust the message's time to - // the BBS local time zone if possible. - var dateTimeStr = ""; - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr); - var useBBSLocalTimeZone = false; - if (msgWrittenLocalTime != -1) - { - dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime); - useBBSLocalTimeZone = true; - } - else - dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, ""); - - var enhHdrLines = enhMsgHdrLines.slice(0); - // Do some things if using the internal header (not loaded externally) - if (this.usingInternalEnhMsgHdr) - { - // If the message is not a poll and contains the properties total_votes and upvotes, - // then put some information in the header containing information about the message's - // voting results. - // Only add the vote information if the total_votes value is non-zero - var msgIsAPoll = false; - if (typeof(MSG_POLL) != "undefined") - msgIsAPoll = Boolean(pMsgHdr.attr & MSG_POLL); - // TODO: Fix the issue with not showing votes. With this, it seems to think it has 0 votes - //var hdrWithVotes = getMsgHdr(this.subBoardCode, false, pMsgHdr.number, true, true); - if (!msgIsAPoll && pMsgHdr.hasOwnProperty("total_votes") && pMsgHdr.hasOwnProperty("upvotes") && pMsgHdr.total_votes != 0) - //if (!msgIsAPoll && hdrWithVotes.hasOwnProperty("total_votes") && hdrWithVotes.hasOwnProperty("upvotes") && hdrWithVotes.total_votes != 0) - { - var voteInfo = getMsgUpDownvotesAndScore(pMsgHdr); - //var voteInfo = getMsgUpDownvotesAndScore(hdrWithVotes); - var voteStatsTxt = "\x01n\x01c" + RIGHT_T_SINGLE + "\x01h\x01gS\x01n\x01gcore\x01h\x01c: \x01b" + voteInfo.voteScore + " (+" + voteInfo.upvotes + ", -" + voteInfo.downvotes + ")\x01n\x01c" + LEFT_T_SINGLE; - enhHdrLines[6] = enhHdrLines[6].substring(0, 10) + "\x01n\x01c" + voteStatsTxt + "\x01n\x01c" + HORIZONTAL_SINGLE + "\x01h\x01k" + enhHdrLines[6].substring(17 + strip_ctrl(voteStatsTxt).length); - } - - // If this is a personal email that has been replied to, then - // put the word "Replied" toward the right of the last line - if (this.readingPersonalEmail && Boolean(pMsgHdr.attr & MSG_REPLIED)) - { - enhHdrLines[6] = enhHdrLines[6].substr(0, enhHdrLines[6].length-17) + "\x01wReplied\x01k" + enhHdrLines[6].substr(enhHdrLines[6].length-10); - } - } - - // 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 header starting on the first column and the given screen row. - var screenX = 1; - var screenY = (typeof(pStartScreenRow) == "number" ? pStartScreenRow : 1); - for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx) - { - console.gotoxy(screenX, screenY++); - console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr, - pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false)); - } - // Older - Used to center the header lines, but I'm not sure this is necessary, - // and it might even make the header off by one, which could be bad. - // 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; - */ - // If avatars are available, then show the sender's avatar on the right - // side - if (this.displayAvatars && (gAvatar != null) && ((pMsgHdr.attr & MSG_ANONYMOUS) == 0)) - { - console.gotoxy(1, screenY-1); - //gAvatar.draw(pMsgHdr.from_ext, pMsgHdr.from, pMsgHdr.from_net_addr, /* above: */true, /* right-justified: */true); - gAvatar.draw(pMsgHdr.from_ext, pMsgHdr.from, pMsgHdr.from_net_addr, /* above: */true, /* right-justified: */this.rightJustifyAvatar); - console.attributes = 0; // Clear the background attribute as the next line might scroll, filling with BG attribute - // If using the traditional (non-scrolling) user interface, then - // put the cursor where it should be. (If using the scrolling - // interface, the cursor will be placed where it should be elsewhere.) - if (!this.scrollingReaderInterface) - console.gotoxy(1, screenY); - } - } - else - { - // The user's terminal doesn't support ANSI - So just output the header - // lines. - for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx) - { - console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr, - pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false)); - } - // Note: Avatar display is only supported for ANSI - } -} - -// For the DigDistMsgReader class: Displays the area chooser header -// -// Parameters: -// pStartScreenRow: The row on the screen at which to start displaying the -// header information. Will be used if the user's terminal -// supports ANSI. -// pClearRowsFirst: Optional boolean - Whether or not to clear the rows first. -// Defaults to true. Only valid if the user's terminal supports -// ANSI. -function DigDistMsgReader_DisplayAreaChgHdr(pStartScreenRow, pClearRowsFirst) -{ - if (this.areaChangeHdrLines == null) - return; - if (this.areaChangeHdrLines.length == 0) - return; - - // If the user's terminal supports ANSI and pStartScreenRow is a number, then - // we can move the cursor and display the header where specified. - if (console.term_supports(USER_ANSI) && (typeof(pStartScreenRow) == "number")) - { - // If specified to clear the rows first, then do so. - var screenX = 1; - var screenY = pStartScreenRow; - var clearRowsFirst = (typeof(pClearRowsFirst) == "boolean" ? pClearRowsFirst : true); - if (clearRowsFirst) - { - console.attributes = "N"; - for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx) - { - console.gotoxy(screenX, screenY++); - console.cleartoeol(); - } - } - // Display the header starting on the first column and the given screen row. - screenX = 1; - screenY = pStartScreenRow; - for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx) - { - console.gotoxy(screenX, screenY++); - console.print(this.areaChangeHdrLines[hdrFileIdx]); - //console.putmsg(this.areaChangeHdrLines[hdrFileIdx]); - //console.cleartoeol("\x01n"); // Shouldn't do this, as it resets color attributes - } - } - else - { - // The user's terminal doesn't support ANSI or pStartScreenRow is not a - // number - So just output the header lines. - for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx) - { - console.print(this.areaChangeHdrLines[hdrFileIdx]); - //console.putmsg(this.areaChangeHdrLines[hdrFileIdx]); - //console.cleartoeol("\x01n"); // Shouldn't do this, as it resets color attributes - console.crlf(); - } - } -} - -// 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.attributes = "N"; - 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("\x01h\x01w"); - console.print("\x01n" + this.colors.scrollbarScrollBlockColor); - wroteBrightBlockColor = true; - wroteDimBlockColor = false; - } - console.print(BLOCK2); - //console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working - ++numSolidBlocksWritten; - } - else - { - if (!wroteDimBlockColor) - { - //console.print("\x01h\x01k"); - console.print("\x01n" + this.colors.scrollbarBGColor); - wroteDimBlockColor = true; - } - console.print(BLOCK1); - //console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working - } - } -} - -// 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_UpdateEnhancedReaderScrollbar(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("\x01n\x01h\x01k"); - console.print("\x01n" + this.colors.scrollbarBGColor); - for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(BLOCK1); - //console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working - } - // Write solid blocks in the new locations - //console.print("\x01w"); - console.print("\x01n" + this.colors.scrollbarScrollBlockColor); - for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(BLOCK2); - //console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working - } - } - else - { - // There is some overlap - // Write dim blocks on top - //console.print("\x01n\x01h\x01k"); - console.print("\x01n" + this.colors.scrollbarBGColor); - for (var screenY = pOldStartRow; screenY < pNewStartRow; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(BLOCK1); - //console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working - } - // Write bright blocks on the bottom - //console.print("\x01w"); - console.print("\x01n" + 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); // TODO: This doesn't seem to be working - } - } - } - 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("\x01n\x01h\x01k"); - console.print("\x01n" + this.colors.scrollbarBGColor); - for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(BLOCK1); - //console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working - } - // Write solid blocks in the new locations - //console.print("\x01w"); - console.print("\x01n" + this.colors.scrollbarScrollBlockColor); - for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY) - { - console.gotoxy(this.msgAreaRight+1, screenY); - console.print(BLOCK2); - //console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working - } - } - else - { - // There is some overlap - // Write bright blocks on top - //console.print("\x01n\x01h\x01w"); - console.print("\x01n" + 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); // TODO: This doesn't seem to be working - } - // Write dim blocks on the bottom - //console.print("\x01k"); - console.print("\x01n" + 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); // TODO: This doesn't seem to be working - } - } - } -} - -// 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) -{ - var msgDeleted = false; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if ((pOffset < 0) || (pOffset >= this.NumMessages(msgbase))) - msgDeleted = true; - else - { - // Get the message's header and see if it's marked as deleted - var msgHdr = this.GetMsgHdrByIdx(pOffset, false, msgbase); - if (msgHdr != null) - msgDeleted = ((msgHdr.attr & MSG_DELETE) == MSG_DELETE); - } - msgbase.close(); - } - - 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; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if (msgbase.cfg != null) - { - // TODO: Update to handle search results? - if ((pOffset >= 0) && (pOffset < 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 msgIdx = msgbase.get_msg_index(true, pOffset, false); - if (msgIdx != null && userHandleAliasNameMatch(msgIdx.to)) - { - var lastMsgOffsetFromUser = -1; - for (var msgOffset = msgbase.total_msgs-1; (msgOffset >= pOffset) && (lastMsgOffsetFromUser == -1); --msgOffset) - { - msgIdx = msgbase.get_msg_index(true, msgOffset, false); - if (msgIdx != null && userHandleAliasNameMatch(msgIdx.to)) - lastMsgOffsetFromUser = msgOffset; - } - // See if the passed-in offset is the last message we found from - // the logged-in user. - msgIstLastFromUser = (lastMsgOffsetFromUser == pOffset); - } - } - } - msgbase.close(); - } - 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("\x01n" + 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.attributes = "N"; - 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, "\x01n", 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("\x01n" + this.colors["msgBodyColor"]); - if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length)) - { - console.print(pMessageLines[msgLineIndex]); // Already shortened to fit - console.print("\x01n" + 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("\x01n" + 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.attributes = "N"; - 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 = "\x01n\x01h\x01k" + 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 = "\x01n\x01h\x01w" + 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("\x01n" + this.colors["msgBodyColor"]); - if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length)) - { - console.print(pMessageLines[msgLineIndex]); // Already shortened to fit - console.print("\x01n" + 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 or undelete 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 -// pDelete: Optional boolean: If true (default), deletes messages; if false, undeletes 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) -// pConfirm: Optional boolean - Whether or not to confirm deleting/undeleting the message. Defaults to true. -// -// Return value: Boolean - Whether or not the message was deleted -function DigDistMsgReader_PromptAndDeleteOrUndeleteMessage(pOffset, pPromptLoc, pDelete, pClearPromptRowAtFirstUse, - pPromptRowWidth, pConfirm) -{ - // Sanity checking - if ((pOffset == null) || (typeof(pOffset) != "number")) - return false; - if (!this.CanDelete() && !this.CanDeleteLastMsg()) - return false; - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - return false; - if ((pOffset < 0) || (pOffset >= this.NumMessages(msgbase))) - { - msgbase.close(); - return false; - } - var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) == "object") && - (typeof(pPromptLoc.x) == "number") && (typeof(pPromptLoc.y) == "number")); - - var deleteMsg = (typeof(pDelete) === "boolean" ? pDelete : true); - - var opSucceeded = false; - - var msgNum = pOffset + 1; - var msgHeader = this.GetMsgHdrByIdx(pOffset, false, msgbase); - if (msgHeader == null) - { - msgbase.close(); - return false; - } - - var errorMessage = ""; - var continueOn = false; - if (deleteMsg) - { - // If it's already marked for deletion, then nothing needs to be done - if ((msgHeader.attr & MSG_DELETE) == MSG_DELETE) - opSucceeded = true; - else - { - // The message is not marked for deletion. - if (this.CanDelete()) - { - if (msgHeader != null) - continueOn = user.is_sysop || userHandleAliasNameMatch(msgHeader.from) || this.readingPersonalEmail; - else - continueOn = false; - } - else if (this.CanDeleteLastMsg()) - { - continueOn = user.is_sysop || this.MessageIsLastFromUser(pOffset); - if (!continueOn) - errorMessage = replaceAtCodesInStr(format(this.text.cannotDeleteMsgText_notLastPostedMsg, msgNum)); - } - else - errorMessage = replaceAtCodesInStr(format(this.text.cannotDeleteMsgText_notYoursNotASysop, msgNum)); - } - } - else - { - // Undeleting a message marked for deletion - if ((msgHeader.attr & MSG_DELETE) == MSG_DELETE) - continueOn = true; - else - opSucceeded = true; // No action needed - } - if (continueOn) - { - // Determine whether or not to delete/undelete 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. - continueOn = true; // True in case of not confirming deletion - var confirmOp = (typeof(pConfirm) == "boolean" ? pConfirm : true); - if (confirmOp) - { - var confirmText = "\x01n"; - if (deleteMsg) - confirmText += replaceAtCodesInStr(format(this.text.msgDelConfirmText, msgNum)); - else - confirmText += replaceAtCodesInStr(format(this.text.msgUndelConfirmText, 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(confirmText) + 5; - var numCharsRemaining = 0; - if (typeof(pPromptRowWidth) == "number") - numCharsRemaining = pPromptRowWidth - promptTxtLen; - else - numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen; - console.attributes = "N"; - 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); - } - continueOn = !console.noyes(confirmText); - } - // If we are to delete/undelete the message, then do it. - if (continueOn) - { - if (deleteMsg) - { - //opSucceeded = msgbase.remove_msg(true, msgHeader.offset); - opSucceeded = msgbase.remove_msg(false, msgHeader.number); - } - else - { - var tmpMsgHdr = msgbase.get_msg_header(false, msgHeader.number, false); - if (tmpMsgHdr != null) - { - tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE; - opSucceeded = msgbase.put_msg_header(false, msgHeader.number, tmpMsgHdr); - } - else - opSucceeded = false; - } - if (opSucceeded) - { - // Delete/undelete any vote response messages for this message - // Delete/undelete vote message headers - var voteDelRetObj = toggleVoteMsgsDeleted(msgbase, msgHeader.number, msgHeader.id, deleteMsg, (this.subBoardCode == "mail")); - // In case there are search results or saved message headers, refresh the header in - // those arrays to enable the deleted attribute. - this.RefreshHdrInSavedArrays(pOffset, MSG_DELETE, deleteMsg); - if (!voteDelRetObj.allVoteMsgsAffected) - { - console.attributes = "N"; - console.crlf(); - console.print("\x01y\x01h* Failed to " + (deleteMsg ? "delete" : "undelete") + " all vote response messages for message " + msgNum + "\x01n"); - console.crlf(); - console.pause(); - } - - // Output a message saying the message has been marked for deletion/undeletion - if (promptLocValid) - console.gotoxy(pPromptLoc); - if (deleteMsg) - console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgDeletedText, msgNum))); - else - console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgUndeletedText, msgNum))); - if (promptLocValid) - console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); - else - { - console.crlf(); - console.pause(); - } - } - } - } - else - { - if (errorMessage.length > 0) - { - if (promptLocValid) - console.gotoxy(pPromptLoc); - console.print(errorMessage); - if (promptLocValid) - console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); - else - { - console.crlf(); - console.pause(); - } - } - } - msgbase.close(); - return opSucceeded; -} - -// 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 -// pDelete: Optional boolean: If true (default), deletes messages; if false, undeletes 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) -// pConfirm: Optional boolean - Whether or not to confirm deleting/undeleting the messages. Defaults to true. -// -// Return value: Boolean - Whether or not all messages were deleted -function DigDistMsgReader_PromptAndDeleteOrUndeleteSelectedMessages(pPromptLoc, pDelete, pClearPromptRowAtFirstUse, - pPromptRowWidth, pConfirm) -{ - var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) === "object") && - (typeof(pPromptLoc.x) === "number") && (typeof(pPromptLoc.y) === "number")); - - var doDelete = (typeof(pDelete) === "boolean" ? pDelete : true); - var allMsgsOpSuccessful = false; - - var errorMsg = ""; // In case anything goes wrong - var continueOn = true; // Whether or not to continue with deletion/undeletion, depending on user confirmation etc. - if (doDelete) - { - // If not all the messages can be deleted, then don't allow it. - continueOn = this.AllSelectedMessagesCanBeDeleted(); - if (!this.AllSelectedMessagesCanBeDeleted()) - { - continueOn = false; - errorMsg = replaceAtCodesInStr(this.text.cannotDeleteAllSelectedMsgsText); - } - } - if (continueOn) - { - // 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 confirmDoIt = (typeof(pConfirm) === "boolean" ? pConfirm : true); - if (confirmDoIt) - { - if (promptLocValid) - { - var promptText = ""; - if (doDelete) - promptText = replaceAtCodesInStr(this.text.delSelectedMsgsConfirmText); - else - promptText = replaceAtCodesInStr(this.text.undelSelectedMsgsConfirmText); - // 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(promptText) + 5; - var numCharsRemaining = 0; - if (typeof(pPromptRowWidth) == "number") - numCharsRemaining = pPromptRowWidth - promptTxtLen; - else - numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen; - console.attributes = "N"; - 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); - continueOn = !console.noyes(promptText); - } - } - // If we are to delete/undelete the messages, then do so. - if (continueOn) - { - // TODO: Return status & error message - var deleteRetObj = this.DeleteOrUndeleteSelectedMessages(doDelete); - allMsgsOpSuccessful = deleteRetObj.opSuccessful; - // If all selected messages were successfully deleted, then output - // a success message. Otherwise, output an error. - var statusMsg = ""; - if (deleteRetObj.opSuccessful) - statusMsg = "\x01n\x01cAll selected messages were " + (doDelete ? "deleted." : "undeleted."); - else - statusMsg = "\x01n\x01h\x01y* Failure to " + (doDelete ? "delete" : "undelete") + " all selected messages"; - if (promptLocValid) - { - console.gotoxy(pPromptLoc); - console.attributes = "N"; - 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(); - } - } - } - - // If there was an error, then display it - if (errorMsg.length > 0) - { - if (promptLocValid) - console.gotoxy(pPromptLoc); - console.print(replaceAtCodesInStr(this.text.cannotDeleteAllSelectedMsgsText)); - if (promptLocValid) - console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS); - else - { - console.crlf(); - console.pause(); - } - } - - return allMsgsOpSuccessful; -} - -/////////////////////////////////////////////////////////////////////////////////// -// 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); - console.putmsg(this.lightbarAreaChooserHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking - console.attributes = "N"; -} - -// 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_WriteGrpListTopHdrLine1(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("\x01n"); -} - -// 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 DigDistMsgReader_WriteSubBrdListHdrLine(pGrpIndex, pNumPages, pPageNum) -{ - var descFormatStr = "\x01n" + this.colors.areaChooserSubBoardHeaderColor + "Sub-boards of \x01h%-25s \x01n" - + 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("\x01n"); -} - -// 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. -// -// Parameters: -// pMsgGrp: Optional boolean - Whether to let the user choose a message group first. -// For internal use. Defaults to true. -// pGrpIdx: If pMsgGrp is true, then this specifies the group index so that -// sub-boards can be displayed. -function DigDistMsgReader_SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx) -{ - // If there are no message groups, then don't let the user - // choose one. - if (msg_area.grp_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no message groups.\r\n\x01p"); - 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; - - var chooseMsgGrp = (typeof(pMsgGrp) == "boolean" ? pMsgGrp : true); - - // This function displays the header line(s) above the list - function displayListHdrLines(pStartRow, pChooseMsgGrp, pReader) - { - console.gotoxy(1, pStartRow); - if (pChooseMsgGrp) - pReader.WriteGrpListHdrLine1(); - else - { - pReader.WriteSubBrdListHdrLine(pGrpIdx); - console.gotoxy(1, pStartRow+1); - printf(pReader.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); - } - } - - // Clear the screen & write the header lines, help line and group list header - console.clear("\x01n"); - this.DisplayAreaChgHdr(1); - displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this); - this.WriteChgMsgAreaKeysHelpLine(); - - // Create a menu of message groups or sub-boards - var msgAreaMenu = (chooseMsgGrp ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pGrpIdx)); - var drawMenu = true; - var lastSearchText = ""; - var lastSearchFoundIdx = -1; - var chosenIdx = -1; - var continueOn = true; - // Let the user choose a group, and also respond to other user choices - while (continueOn) - { - chosenIdx = -1; - var msgGrpIdx = msgAreaMenu.GetVal(drawMenu); - drawMenu = true; - var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) == "string" ? msgAreaMenu.lastUserInput.toUpperCase() : msgAreaMenu.lastUserInput); - if (typeof(msgGrpIdx) == "number") - chosenIdx = msgGrpIdx; - // If userChoice is not a number, then it should be null in this case, - // and the user would have pressed one of the additional quit keys set - // up for the menu. So look at the menu's lastUserInput and do the - // appropriate thing. - else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit - continueOn = false; - else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_rows); - var promptText = "Search: "; - console.print(promptText); - var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS); - lastSearchText = searchText; - // If the user entered text, then do the search, and if found, - // found, go to the page and select the item indicated by the - // search. - if (searchText.length > 0) - { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx; - var idx = -1; - if (chooseMsgGrp) - idx = findMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx); - else - idx = findSubBoardIdxFromText(pGrpIdx, searchText, msgAreaMenu.selectedItemIdx+1); - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else - { - // If the current index and the last index are both on the same page on the - // menu, then have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - if (chooseMsgGrp) - idx = findMsgGrpIdxFromText(searchText, 0); - else - idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0); - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("Not found"); - drawMenu = false; - } - } - } - else - drawMenu = false; - this.WriteChgMsgAreaKeysHelpLine(); - } - else if (lastUserInputUpper == "N") // Next search result (requires an existing search term) - { - // This works but seems a little strange sometimes. - // - Should this always start from the selected index? - // - If it wraps around to one of the items on the first page, - // should it always set the top index to 0? - if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1)) - { - var oldLastSearchFoundIdx = lastSearchFoundIdx; - var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx; - // Do the search, and if found, go to the page and select the item - // indicated by the search. - var idx = 0; - if (chooseMsgGrp) - idx = findMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1); - else - idx = findSubBoardIdxFromText(pGrpIdx, searchText, lastSearchFoundIdx+1); - if (idx > -1) - { - lastSearchFoundIdx = idx; - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - { - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - if (msgAreaMenu.topItemIdx < 0) - msgAreaMenu.topItemIdx = 0; - } - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - if (chooseMsgGrp) - idx = findMsgGrpIdxFromText(searchText, 0); - else - idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0); - lastSearchFoundIdx = idx; - if (idx > -1) - { - // Set the currently selected item in the menu, and ensure it's - // visible on the page - msgAreaMenu.selectedItemIdx = idx; - if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage()) - { - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1; - if (msgAreaMenu.topItemIdx < 0) - msgAreaMenu.topItemIdx = 0; - } - else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx) - msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx; - else - { - // The current index and the last index are both on the same page on the - // menu, so have the menu only redraw those items. - msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx]; - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("Not found"); - drawMenu = false; - this.WriteChgMsgAreaKeysHelpLine(); - } - } - } - else - { - this.WriteLightbarKeyHelpErrorMsg("There is no previous search", REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE); - drawMenu = false; - this.WriteChgMsgAreaKeysHelpLine(); - } - } - else if (lastUserInputUpper == "?") // Show help - { - this.ShowChooseMsgAreaHelpScreen(true, true); - console.pause(); - // Refresh the screen - console.clear("\x01n"); - console.gotoxy(1, 1); - this.DisplayAreaChgHdr(1); - displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this); - this.WriteChgMsgAreaKeysHelpLine(); - } - // If the user entered a numeric digit, then treat it as - // the start of the message group number. - if (lastUserInputUpper.match(/[0-9]/)) - { - // Put the user's input back in the input buffer to - // be used for getting the rest of the message number. - console.ungetstr(lastUserInputUpper); - // 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("\x01n"); - console.print("\x01cChoose group #: \x01h"); - var userInput = console.getnum(msg_area.grp_list.length); - if (userInput > 0) - chosenIdx = userInput - 1; - else - { - // The user didn't make a selection. So, we need to refresh - // the screen due to everything being moved up one line. - displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this); - this.WriteChgMsgAreaKeysHelpLine(); - } - } - - // If a group/sub-board was chosen, then deal with it. - if (chosenIdx > -1) - { - // If choosing a message group, then let the user choose a - // sub-board within the group. Otherwise, return the user's - // chosen sub-board. - if (chooseMsgGrp) - { - //SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx) - // Show a "Loading..." text in case there are many sub-boards in - // the chosen message group - console.crlf(); - console.print("\x01nLoading..."); - console.line_counter = 0; // To prevent a pause before the message list comes up - // Ensure that the sub-board printf information is created for - // the chosen message group. - this.BuildSubBoardPrintfInfoForGrp(chosenIdx); - var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(false, chosenIdx); - if (chosenSubBoardIdx > -1) - { - // Set the current group & sub-board - bbs.curgrp = chosenIdx; - bbs.cursub = chosenSubBoardIdx; - continueOn = false; - } - else - { - // A sub-board was not chosen, so we'll have to re-draw - // the header and list of message groups. - displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this); - } - } - else - return chosenIdx; // Return the chosen sub-board index - } - } - - // 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); - } -} - -// 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() -{ - // TODO: Allow searching - // If there are no message groups, then don't let the user - // choose one. - if (msg_area.grp_list.length == 0) - { - console.clear("\x01n"); - console.print("\x01y\x01hThere are no message groups.\r\n\x01p"); - 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 grpSearchText = ""; - 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("\x01n"); - this.DisplayAreaChgHdr(); - //console.crlf(); - this.ListMsgGrps(grpSearchText); - console.crlf(); - console.print("\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c, \x01hQ\x01n\x01cuit, or [\x01h" + - +(msg_area.sub[this.subBoardCode].grp_index+1) + "\x01n\x01c]: \x01h"); - // Accept Q (quit), / or CTRL_F (Search) or a file library number - selectedGrp = console.getkeys("Q/" + CTRL_F, 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; - // / or CTRL-F: Search - else if ((selectedGrp.toString() == "/") || (selectedGrp.toString() == CTRL_F)) - { - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - if (searchText.length > 0) - grpSearchText = searchText; - } - 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 subSearchText = ""; - var continueChoosingSubBoard = true; - while (continueChoosingSubBoard) - { - console.clear("\x01n"); - this.DisplayAreaChgHdr(); - this.ListSubBoardsInMsgGroup(selectedGrp-1, defaultSubBoard-1, null, subSearchText); - console.crlf(); - console.print("\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c, \x01hQ\x01n\x01cuit, or [\x01h" + - defaultSubBoard + "\x01n\x01c]: \x01h"); - // Accept Q (quit), / or CTRL_F (Search) or a sub-board number - selectedSubBoard = console.getkeys("Q/" + CTRL_F, 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; - // / or CTRL-F: Search - else if ((selectedSubBoard.toString() == "/") || (selectedSubBoard.toString() == CTRL_F)) - { - console.crlf(); - var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n"; - console.print(searchPromptText); - var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE); - if (searchText.length > 0) - subSearchText = searchText; - } - // 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("\x01n\x01h\x01y" + 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); - } -} - -// For the DigDistMsgReader class: Lists all message groups (for the traditional -// user interface). -// -// Parameters: -// pSearchText: Optional - Search text for the message groups -function DigDistMsgReader_ListMsgGrps_Traditional(pSearchText) -{ - // Print the header - this.WriteGrpListHdrLine1(); - console.attributes = "N"; - - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - - // List the message groups - var printIt = true; - for (var i = 0; i < msg_area.grp_list.length; ++i) - { - if (searchText.length > 0) - printIt = ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[i].description.toUpperCase().indexOf(searchText) >= 0)); - else - printIt = true; - - if (printIt) - { - 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 -// pSearchText: Optional - Search text for the message sub-boards -function DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIndex, pSortType, pSearchText) -{ - // 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.WriteSubBrdListHdrLine(grpIndex); - console.crlf(); - printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time"); - console.attributes = "N"; - - // List each sub-board in the message group. - var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : ""); - var subBoardArray = null; // For sorting, if desired - var newestDate = {}; // 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) - var includeSubBoard = true; - // 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 = []; - var subBoardInfo = null; - for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list) - { - if (searchText.length > 0) - includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.toUpperCase().indexOf(searchText) >= 0)); - else - includeSubBoard = true; - if (!includeSubBoard) - continue; - - // 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; - // Note: numReadableMsgs() is slow because it goes through and - // checks for deleted messages, etc., so just use msgBase.total_msgs - //subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code); - subBoardInfo.numPosts = msgBase.total_msgs; - - // Get the date & time when the last message was imported. - if (subBoardInfo.numPosts > 0) - { - var msgIdx = msgBase.total_msgs-1; - msgHeader = msgBase.get_msg_index(true, msgIdx, false); - while (!isReadableMsgHdr(msgHeader, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code) && (msgIdx >= 0)) - msgHeader = msgBase.get_msg_index(true, --msgIdx, true); - if (msgHeader != null) - msgHeader = msgBase.get_msg_header(true, msgIdx, false); - if (msgHeader != null) - { - if (this.msgAreaList_lastImportedMsg_showImportTime) - subBoardInfo.newestPostDate = msgHeader.when_imported_time; - else - { - //subBoardInfo.newestPostDate = msgHeader.when_written_time; - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalTime != -1) - subBoardInfo.newestPostDate = msgWrittenTimeToLocalBBSTime(msgHeader); - else - subBoardInfo.newestPostDate = msgHeader.when_written_time; - } - } - } - } - msgBase.close(); - subBoardArray.push(subBoardInfo); - } - - // 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) ? "\x01n" + - 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) - { - if (searchText.length > 0) - includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.toUpperCase().indexOf(searchText) >= 0)); - else - includeSubBoard = true; - if (!includeSubBoard) - continue; - - // 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. - // Note: numReadableMsgs() is slow because it goes through and - // checks for deleted messages, etc., so just use msgBase.total_msgs - //var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code); - var numMsgs = msgBase.total_msgs; - if (numMsgs > 0) - { - var msgIdx = msgBase.total_msgs-1; - msgHeader = msgBase.get_msg_index(true, msgIdx, false); - while (!isReadableMsgHdr(msgHeader, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code) && (msgIdx >= 0)) - msgHeader = msgBase.get_msg_index(true, --msgIdx, true); - if (msgHeader != null) - msgHeader = msgBase.get_msg_header(true, msgIdx, false); - if (msgHeader != null) - { - // 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); - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalTime != -1) - { - newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalTime); - newestDate.time = strftime("%H:%M:%S", msgWrittenLocalTime); - } - 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) ? "\x01n" + - 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), - numMsgs, newestDate.date, newestDate.time); - - msgBase.close(); - } - } - } -} - -////////////////////////////////////////////// -// Message group list stuff (lightbar mode) // -////////////////////////////////////////////// - -// 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.attributes = "N"; - // 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("\x01n"); -} - -////////////////////////////////////////////////// -// 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+this.areaChangeHdrLines.length); - console.print("\x01n" + this.colors.areaChooserMsgAreaHeaderColor + pPageNum + " of " + - pNumPages + ") "); - } - else - { - console.gotoxy(51, 1+this.areaChangeHdrLines.length); - console.print("\x01n" + this.colors.areaChooserSubBoardHeaderColor + pPageNum + " of " + - pNumPages + ") "); - } - - if (pRestoreCurPos) - console.gotoxy(originalCurPos); -} - -// For the DigDistMsgReader class: Returns a formatted string with sub-board -// information 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 (assumed to be valid) -// pHighlight: Boolean - Whether or not to write the line highlighted. -// -// Return value: A string with the sub-board information -function DigDistMsgReader_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight) -{ - // 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)); - - var subBoardStr = ""; - // Use the highlight background color if pHighlight is true. - if (pHighlight) - subBoardStr += this.colors.areaChooserMsgAreaBkgHighlightColor; - - var subBoardInfo = getSubBoardInfo(pGrpIndex, pSubIndex, this.msgAreaList_lastImportedMsg_showImportTime); - var latestDateStr = strftime("%Y-%m-%d", subBoardInfo.newestTime); - var latestTimeStr = strftime("%H:%M:%S", subBoardInfo.newestTime); - subBoardStr += (currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " "); - subBoardStr += format((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), - subBoardInfo.numItems, latestDateStr, latestTimeStr); - return subBoardStr; -} - -/////////////////////////////////////////////// -// 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("\x01n"); - else - console.attributes = "N"; - DisplayProgramInfo(); - console.crlf(); - console.print("\x01n\x01c\x01hMessage area (sub-board) chooser"); - console.crlf(); - console.print("\x01k" + charStr(HORIZONTAL_SINGLE, 32) + "\x01n"); - console.crlf(); - console.print("\x01cFirst, 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(); - - console.crlf(); - console.print("Keyboard commands:"); - console.crlf(); - console.print("\x01k\x01h" + charStr(HORIZONTAL_SINGLE, 18) + "\x01n"); - console.crlf(); - console.print("\x01n\x01c\x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c: Find group/sub-board"); - console.crlf(); - console.print("\x01n\x01c\x01h?\x01n\x01c: Show this help screen"); - console.crlf(); - console.print("\x01hQ\x01n\x01c: Quit"); - console.crlf(); - - if (pLightbar) - { - console.crlf(); - console.print("\x01n\x01cThe lightbar interface also allows up & down navigation through the lists:"); - console.crlf(); - console.print("\x01k\x01h" + charStr(HORIZONTAL_SINGLE, 74)); - console.crlf(); - console.print("\x01n\x01c\x01hUp\x01n\x01c/\x01hdown arrow\x01n\x01c: Move the cursor up/down one line"); - console.crlf(); - console.print("\x01hPageUp\x01n\x01c/\x01hPageDown\x01n\x01c: Move up/down a page"); - console.crlf(); - console.print("\x01hENTER\x01n\x01c: Select the current group/sub-board"); - console.crlf(); - console.print("\x01hHOME\x01n\x01c: Go to the first item on the screen"); - console.crlf(); - console.print("\x01hEND\x01n\x01c: Go to the last item on the screen"); - console.crlf(); - console.print("\x01hF\x01n\x01c: Go to the first page"); - console.crlf(); - console.print("\x01hL\x01n\x01c: Go to the last page"); - console.crlf(); - console.print("\x01hN\x01n\x01c: Next search result"); - 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] = {}; - 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 = - "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " " - + "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaNumHighlightColor - + "%" + this.areaNumLen + "d \x01n" - + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaDescHighlightColor + "%-" - + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \x01n" - + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%" - + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d \x01n" - + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaDateHighlightColor + "%" + this.dateLen + "s \x01n" - + this.colors.areaChooserMsgAreaBkgHighlightColor - + this.colors.areaChooserMsgAreaTimeHighlightColor + "%" + this.timeLen + "s\x01n"; - } -} - -// 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: -// pSubCodeOrMsgbase: An internal sub-board code of the messagebase, or a MsgBase object representing -// the sub-board the message is in (assumed to be open) -// pMsgNum: The message number of the message headers to retrieve -// pKludgeOnly: Boolean - Whether or not to only get the kludge lines. If false, -// then all header fields will be retrieved. -// pUseColors: Optional boolean: Whether or not to add colors to the strings. Defaults to true. -// pWordWrap: Optional boolean: Whether or not to word-wrap the header lines to the user's -// terminal width. Defaults to true. -// pPrependHdrLabelLines: Optional boolean - Whether or not to prepend a couple lines labeling -// that these are header/kludge lines. Defaults to true. -// -// Return value: An array of strings containing the extended message header information -function DigDistMsgReader_GetExtdMsgHdrInfo(pSubCodeOrMsgbase, pMsgNum, pKludgeOnly, pUseColors, pWordWrap, pPrependHdrLabelLines) -{ - if (typeof(pMsgNum) !== "number") - return []; - - // Get the message header with fields expanded so we can get the most info possible. - var msgHdr = null; - if (typeof(pSubCodeOrMsgbase) === "string") - { - var msgbase = new MsgBase(pSubCodeOrMsgbase); - if (msgbase.open()) - { - // TODO: I think we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - msgHdr = msgbase.get_msg_header(false, pMsgNum, true, true); - msgbase.close(); - } - } - else if (typeof(pSubCodeOrMsgbase) === "object" && pSubCodeOrMsgbase.hasOwnProperty("get_msg_header") && pSubCodeOrMsgbase.is_open) - msgHdr = pSubCodeOrMsgbase.get_msg_header(false, pMsgNum, true, true); - - if (msgHdr == null) - return []; - - // The message header retrieved that way might not have vote information, - // so copy any additional header information from this.hdrsForCurrentSubBoard - // if there's a header there for this message. - if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum)) - { - var tmpHdrIdx = this.msgNumToIdxMap[pMsgNum]; - if (this.hdrsForCurrentSubBoard.hasOwnProperty(tmpHdrIdx)) - { - for (var hdrProp in this.hdrsForCurrentSubBoard[tmpHdrIdx]) - { - if (!msgHdr.hasOwnProperty(hdrProp)) - msgHdr[hdrProp] = this.hdrsForCurrentSubBoard[tmpHdrIdx][hdrProp]; - } - } - } - - var kludgeOnly = (typeof(pKludgeOnly) == "boolean" ? pKludgeOnly : false); - var useColors = (typeof(pUseColors) === "boolean" ? pUseColors : true); - var wordWrap = (typeof(pWordWrap) === "boolean" ? pWordWrap : true); - var prependHdrLabelLines = (typeof(pPrependHdrLabelLines) === "boolean" ? pPrependHdrLabelLines : true); - - - - var msgHdrInfoLines = []; - - var formatStr; - if (useColors) - formatStr = "\x01n" + this.colors.hdrLineLabelColor + "%s: \x01n" + this.colors.hdrLineValueColor + "%-s"; - else - formatStr = "%s: %-s"; - - // Make an array of objects with 'prop' and 'textArray' fields; this array will be sorted by prop - var msgInfoObjs = []; - for (var prop in msgHdr) - { - if (prop == "field_list") - { - //var fieldListLines = this.GetMsgHdrFieldListText(msgHdr[prop], true); - //for (var i = 0; i < fieldListLines.length; ++i) - // msgHdrInfoLines.push(fieldListLines[i]); - - var fieldListObj = GetMsgHdrFieldListObj(msgHdr.field_list); - var fieldListKeys = Object.keys(msgHdr); - fieldListKeys.sort(); - for (var fieldListProp in fieldListObj) - //for (var fieldListKeyIdx = 0; fieldListKeyIdx < fieldListKeys.length; ++fieldListKeyIdx) - { - //var fieldListProp = fieldListKeys[fieldListKeyIdx]; - //msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp])); - if (Array.isArray(fieldListObj[fieldListProp])) - { - var tmpTxtLines = []; - if (fieldListObj[fieldListProp].length > 0) - { - //msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp][0])); - tmpTxtLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp][0])); - for (var i = 1; i < fieldListObj[fieldListProp].length; ++i) - { - //msgHdrInfoLines.push(fieldListObj[fieldListProp][i]); - tmpTxtLines.push(fieldListObj[fieldListProp][i]); - } - } - msgInfoObjs.push({ - 'prop': fieldListProp, - 'textArray': tmpTxtLines - }); - } - else - { - //msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp])); - msgInfoObjs.push({ - 'prop': fieldListProp, - 'textArray': [ format(formatStr, fieldListProp, fieldListObj[fieldListProp]) ] - }) - } - } - - } - else - { - var addIt = kludgeOnly ? MsgHdrPropIsKludgeLine(prop) : true; - if (addIt) - { - // Remove underscores from the property for the label - var 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); - - // Value - var propValue = ""; - if (typeof(msgHdr[prop]) === "function") // Such as get_rfc822_header - continue; - if (prop == "when_written_time") //itemValue = system.timestr(msgHdr.when_written_time); - propValue = system.timestr(msgHdr.when_written_time) + " " + system.zonestr(msgHdr.when_written_zone); - else if (prop == "when_imported_time") //propValue = system.timestr(msgHdr.when_imported_time); - propValue = system.timestr(msgHdr.when_imported_time) + " " + system.zonestr(msgHdr.when_imported_zone); - else if ((prop == "when_imported_zone") || (prop == "when_written_zone")) - propValue = system.zonestr(msgHdr[prop]); - else if (prop == "attr") - propValue = makeMainMsgAttrStr(msgHdr[prop], "None"); - else if (prop == "auxattr") - propValue = makeAuxMsgAttrStr(msgHdr[prop], "None"); - else if (prop == "netattr") - propValue = makeNetMsgAttrStr(msgHdr[prop], "None"); - else - propValue = msgHdr[prop]; - if (typeof(propValue) === "string") - { - // Replace tabs with spaces, and strip CRLF characters - // TODO: Should CRLF characters actually be split into separate lines? - propValue = propValue.trim().replace(/\t/g, " ").replace(/[^\x20-\x7E]/g, ''); - propValue = propValue.replace(/\r\n/g, ""); - propValue = propValue.replace(/\n/g, "").replace(/\r/g, ""); - } - - //msgHdrInfoLines.push(format(formatStr, propLabel, propValue)); - msgInfoObjs.push({ - 'prop': propLabel, - 'textArray': [ format(formatStr, propLabel, propValue) ] - }) - } - } - } - // Sort the header lines alphabetically - msgInfoObjs.sort(function(pA, pB) - { - if (pA.prop < pB.prop) - return -1; - else if (pA.prop == pB.prop) - return 0; - else - return 1; - }); - for (var i = 0; i < msgInfoObjs.length; ++i) - { - for (var j = 0; j < msgInfoObjs[i].textArray.length; ++j) - msgHdrInfoLines.push(msgInfoObjs[i].textArray[j]); - } - // Free some memory - for (var prop in msgInfoObjs) - delete msgInfoObjs[prop]; - - - // If the caller wants to word-wrap, make sure the header lines aren't too long for the - // user's terminal. And leave a column for the scrollbar. - var hdrInfoLinesWrapped; - if (wordWrap) - { - hdrInfoLinesWrapped = []; - var maxLen = console.screen_columns - 1; - var colorFormatStr = "\x01n" + this.colors.hdrLineLabelColor + "%-s: \x01n" + this.colors.hdrLineValueColor + "%-s"; - for (var i = 0; i < msgHdrInfoLines.length; ++i) - { - //var wrappedLines = word_wrap(msgHdrInfoLines[i], maxLen).split("\n"); - var wrappedLines = lfexpand(word_wrap(msgHdrInfoLines[i], maxLen)).split("\r\n"); - for (var wrappedI = 0; wrappedI < wrappedLines.length; ++wrappedI) - { - if (wrappedLines[wrappedI].length == 0) continue; - hdrInfoLinesWrapped.push(wrappedLines[wrappedI]); - } - } - } - else // No word wrapping - hdrInfoLinesWrapped = msgHdrInfoLines; - - // 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 (hdrInfoLinesWrapped.length > 0) - { - if (prependHdrLabelLines) - { - if (kludgeOnly) - { - hdrInfoLinesWrapped.splice(0, 0, "\x01n\x01c\x01hMessage Information/Kludge Lines\x01n"); - hdrInfoLinesWrapped.splice(1, 0, "\x01n\x01g\x01h--------------------------------\x01n"); - } - else - { - hdrInfoLinesWrapped.splice(0, 0, "\x01n\x01c\x01hMessage Headers\x01n"); - hdrInfoLinesWrapped.splice(1, 0, "\x01n\x01g\x01h---------------\x01n"); - } - } - if (hdrInfoLinesWrapped[hdrInfoLinesWrapped.length-1].length == 0) - hdrInfoLinesWrapped.pop(); - } - - return hdrInfoLinesWrapped; -} - -// For the DDMsgReader class: Helper for GetExtdMsgHdrInfo() - Gets -// text lines for the field_list property in a message header -// -// Parameters: -// pHdrFieldList: The value of the field_list property in a message header -// pUseColors: Boolean - Whether or not to add attribute codes to the lines -// -// Return value: An array with text lines for the field list, with colors -// for displaying message header information -function DigDistMsgReader_GetMsgHdrFieldListText(pHdrFieldList, pUseColors) -{ - var textLines = []; - - // 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 = { - numNonBlankLines: 0, - firstNonBlankLineIdx: -1, - 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; - } - - var fieldsAndValues = {}; - - var hdrFieldLabel = ""; - var lastHdrFieldLabel = null; - var addBlankLineAfterIdx = -1; - for (var fieldI = 0; fieldI < pHdrFieldList.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) - if (pUseColors) - hdrFieldLabel = "\x01n" + this.colors.hdrLineLabelColor + msgHdrFieldListTypeToLabel(pHdrFieldList[fieldI].type) + "\x01n"; - else - hdrFieldLabel = msgHdrFieldListTypeToLabel(pHdrFieldList[fieldI].type); - hdrFieldLabel = hdrFieldLabel.replace(/\t/g, " "); - fieldsAndValues[hdrFieldLabel] = true; // TODO: Change to the actual text - var infoLineWrapped = pHdrFieldList[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 = ""; - if (pUseColors) - hdrItem = "\x01n" + this.colors.hdrLineValueColor + infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx] + "\x01n"; - else - hdrItem = infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx]; - hdrItem = hdrItem.replace(/\t/g, " "); - // If the header field label is different, then add it to the - // header info lines - if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) - { - var numFieldItemsWithSameType = fieldListCountSameTypes(pHdrFieldList, fieldI); - if (numFieldItemsWithSameType > 1) - { - textLines.push(""); - textLines.push(hdrFieldLabel); - addExtraBlankLineAtEnd = true; - addBlankLineAfterIdx = fieldI + numFieldItemsWithSameType - 1; - } - else - { - hdrItem = hdrFieldLabel + " " + hdrItem; - numFieldItemsWithSameType = -1; - } - } - textLines.push(hdrItem); - /* - if (console.strlen((hdrItem) < this.msgAreaWidth) - textLines.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)) - textLines.push(""); - textLines.push(hdrFieldLabel); - //textLines.push(infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx]); - if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) - textLines.push(""); - } - */ - } - else - { - // If the header field label is different, then add it to the - // header info lines - if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) - { - textLines.push(""); - textLines.push(hdrFieldLabel); - } - var infoLineWrapped = pHdrFieldList[fieldI].data; - var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n"); - var preAttrs = pUseColors ? "\x01n" + this.colors.hdrLineValueColor : ""; - var postAttrs = pUseColors ? "\x01n" : ""; - for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx) - { - if (infoLineWrappedArray[lineIdx].length > 0) - textLines.push(preAttrs + infoLineWrappedArray[lineIdx] + postAttrs); - } - // If the header field label is different, then add a blank line to the - // header info lines - if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel)) - textLines.push(""); - } - if (addBlankLineAfterIdx == fieldI) - textLines.push(""); - } - lastHdrFieldLabel = hdrFieldLabel; - } - - // For each line, replace tabs with spaces, remove any unprintable characters, - // and in case any line has any CRLF characters, split the lines on CRLF - for (var i = 0; i < textLines.length; ++i) - { - textLines[i] = textLines[i].replace(/\t/g, " "); - //textLines[i] = textLines[i].replace(/\r|\n/g, ""); - var array = textLines[i].split("\r\n"); - if (array.length > 1) - { - textLines[i] = array[0]; - for (var array2Idx = 1; array2Idx < array.length; ++array2Idx) - { - if (array[array2Idx].length > 0) - textLines.splice(i+array2Idx, 0, array[array2Idx]); - } - } - //textLines[i] = textLines[i].replace(/[^\x20-\x7E]/g, ''); // Remove unprintable characters - } - - return textLines; -} -// Helper for GetExtdMsgHdrInfo() - For the "field_list" -// property of a message header, this gathers the field labels & values into an -// object (where the object properties are the labels) -// -// Parameters: -// pHdrFieldArray: The value of the field_list property in a message header -// (this is normally an array of objects containing 'type' and -// 'data' properties) -// -// Return value: An object where the properties are the field labels, and the -// value for each is usually an array of strings -function GetMsgHdrFieldListObj(pHdrFieldArray) -{ - if (!Array.isArray(pHdrFieldArray)) - return {}; - - // 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 = { - numNonBlankLines: 0, - firstNonBlankLineIdx: -1, - 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; - } - - var fieldListObj = {}; - for (var fieldI = 0; fieldI < pHdrFieldArray.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) - var hdrFieldLabel = msgHdrFieldListTypeToLabel(pHdrFieldArray[fieldI].type, false); - hdrFieldLabel = hdrFieldLabel.replace(/\t/g, " "); - - // Add the data to fieldListObj, with the label as the property - //fieldListObj[hdrFieldLabel] = pHdrFieldArray[fieldI].data.replace(/\t/g, " "); - // Make the data an array of strings, split based on CRLF characters - var infoLineWrappedArray = lfexpand(pHdrFieldArray[fieldI].data).replace(/\t/g, " ").split("\r\n"); - // If any of the strings starts with a date/time ("WhenExported" or "WhenImported"), - // then format it so that the date & time are more readable - for (var i = 0; i < infoLineWrappedArray.length; ++i) - { - if ((infoLineWrappedArray[i].indexOf("WhenExported") == 0 || infoLineWrappedArray[i].indexOf("WhenImported") == 0) && infoLineWrappedArray[i].length >= 28) - { - //system.timestr(msgHdr.when_imported_time) + " " + system.zonestr(msgHdr.when_imported_zone) - var firstPart = infoLineWrappedArray[i].substr(0, 14); - var yearStr = infoLineWrappedArray[i].substr(14, 4); - var monthStr = infoLineWrappedArray[i].substr(18, 2); - var dayStr = infoLineWrappedArray[i].substr(20, 2); - var hourStr = infoLineWrappedArray[i].substr(22, 2); - var minStr = infoLineWrappedArray[i].substr(24, 2); - var secStr = infoLineWrappedArray[i].substr(26, 2); - var remaining = infoLineWrappedArray[i].substr(28); - infoLineWrappedArray[i] = format("%s%s-%s-%s %s:%s:%s %s", firstPart, yearStr, monthStr, dayStr, hourStr, minStr, secStr, remaining); - } - } - if (!fieldListObj.hasOwnProperty(hdrFieldLabel)) - fieldListObj[hdrFieldLabel] = infoLineWrappedArray; - else - { - // Append to the data already there - fieldListObj[hdrFieldLabel].push(""); - for (var i = 0; i < infoLineWrappedArray.length; ++i) - fieldListObj[hdrFieldLabel].push(infoLineWrappedArray[i]); - } - } - - return fieldListObj; -} - -// Returns whether a message header property name can be considered a "kludge line" -function MsgHdrPropIsKludgeLine(pPropName) -{ - if (typeof(pPropName) !== "string" || pPropName.length == 0) - return false; - - var propNameUpper = pPropName.toUpperCase(); - return (propNameUpper == "FTN_MSGID" || propNameUpper == "FTN_REPLY" || - propNameUpper == "FTN_AREA" || propNameUpper == "FTN_FLAGS" || - propNameUpper == "FTN_PID" || propNameUpper == "FTN_TID" || - propNameUpper == "WHEN_WRITTEN_TIME" || propNameUpper == "WHEN_IMPORTED_TIME"); -} - -// 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 -// hasAttachments: Boolean - Whether or not the message has attachments -// attachments: An array of the attached filenames (as strings) -// errorMsg: An error message, if something bad happened -function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments, - pGetB64Data, pMsgBody) -{ - var retObj = { - msgText: "", - messageLines: [], - topMsgLineIdxForLastPage: 0, - msgFractionShown: 0.0, - numSolidScrollBlocks: 0, - numNonSolidScrollBlocks: 0, - solidBlockStartRow: 0, - hasAttachments: false, - attachments: [], - errorMsg: "" - }; - - var determineAttachments = (typeof(pDetermineAttachments) == "boolean" ? pDetermineAttachments : true); - var getB64Data = (typeof(pGetB64Data) == "boolean" ? pGetB64Data : true); - var msgBody = ""; - if (typeof(pMsgBody) == "string") - msgBody = pMsgBody; - else - { - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true); - msgbase.close(); - } - else - { - retObj.errorMsg = "Unable to open the sub-board"; - return retObj; - } - } - retObj.msgText = word_wrap(msgBody, console.screen_columns - 1, true); - - var msgTextAltered = retObj.msgText; // Will alter the message text, but not yet - // Only interpret @-codes if the user is reading personal email and the sender is a sysop. 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 && msgSenderIsASysop(pMsgHdr)) - msgTextAltered = replaceAtCodesInStr(msgTextAltered); // 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. - msgTextAltered = convertAttrsToSyncPerSysCfg(msgTextAltered, false); - // If configured to convert Y-style MCI attribute codes to Synchronet attribute codes, then do so - if (this.convertYStyleMCIAttrsToSync) - msgTextAltered = YStyleMCIAttrsToSyncAttrs(msgTextAltered); - - // If this is a message with a "By: <name> to <name>" and a date, then - // sometimes such a message might have enter characters (ASCII 13), which - // can mess up the display of the message, so remove enter characters - // from the beginning of the message. - var msgTextWithoutAttrs = strip_ctrl(msgTextAltered); - var fromToSearchStr = "By: " + pMsgHdr.from + " to " + pMsgHdr.to; - var toFromSearchStr = "By: " + pMsgHdr.to + " to " + pMsgHdr.from; - var fromToStrIdx = msgTextWithoutAttrs.indexOf(fromToSearchStr); - var toFromStrIdx = msgTextWithoutAttrs.indexOf(toFromSearchStr); - var strIdx = -1; - if (fromToStrIdx > -1) - strIdx = fromToStrIdx; - else if (toFromStrIdx > -1) - strIdx = toFromStrIdx; - if (strIdx > -1) - { - // " on Mon Feb 13 2017 01:00 pm " // 29 characters long - strIdx += toFromSearchStr.length + 29 + 37; // 37: Extra room for Synchronet attribute codes - // Remove enter characters from the beginning of the message - var tmpStr = msgTextAltered.substring(0, strIdx).replace(ascii(13), ""); - msgTextAltered = tmpStr + msgTextAltered.substr(strIdx); - // To remove the "By: <name> to <name> on <date>" lines altogether: - //msgTextAltered = msgTextAltered.substr(strIdx); - } - - // ASCII 0x8D (141 decimal) is considered a soft-CR character in FidoNet. In - // echocfg, under Global Settings, the setting "Strip Incoming Soft-CRs" can - // be enabled to remove those characters (that are not UTF-8 - // encoded). - //msgTextAltered = msgTextAltered.replace(new RegExp("\x8D", "g"), ""); // 'i' with accent; hard word wrap character - //msgTextAltered = msgTextAltered.replace(new RegExp("\x8D", "g"), "\r\n"); // 'i' with accent; hard word wrap character - // This PDF lists some characters without any description - Are these normally non-printable? - // https://www.utm.edu/staff/lholder/csci201/ascii_table.pdf - // 0x8D (141) is 'i' with accent; hard word wrap character - //msgTextAltered = msgTextAltered.replace(new RegExp("[\x81\x8D\x8F\x90\x9D]", "g"), ""); - - 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; -} - -// Shows a message hex dump with a scrollable interface -// -// Parameters: -// pMsgHexInfo: An object with message hex & scrollbar information, as returned by GetMsgHexInfo() -function DigDistMsgReader_ShowMsgHex_Scrolling(pMsgHexInfo) -{ - if (typeof(pMsgHexInfo) !== "object" || !Array.isArray(pMsgHexInfo.msgHexArray) || pMsgHexInfo.msgHexArray.length == 0) - return; - - var msgReaderObj = this; - var lastInfoSolidBlockStartRow = this.msgAreaTop; - - // This is a scrollbar update function for use when viewing the header info/kludge lines. - function msgHexScrollbarUpdateFn(pFractionToLastPage) - { - var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(pMsgHexInfo.numNonSolidScrollBlocks * pFractionToLastPage); - if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow) - msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, pMsgHexInfo.numSolidScrollBlocks); - lastInfoSolidBlockStartRow = infoSolidBlockStartRow; - console.gotoxy(1, console.screen_rows); - } - - if (pMsgHexInfo.msgHexArray.length > 0) - { - var msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1; - var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1; - if (this.userSettings.useEnhReaderScrollbar) - this.DisplayEnhancedReaderWholeScrollbar(pMsgHexInfo.solidBlockStartRow, pMsgHexInfo.numSolidScrollBlocks); - scrollTextLines(pMsgHexInfo.msgHexArray, 0, this.colors.msgBodyColor, true, - this.msgAreaLeft, this.msgAreaTop, msgAreaWidth, msgAreaHeight, - 1, console.screen_rows, this.userSettings.useEnhReaderScrollbar, - msgHexScrollbarUpdateFn); - } -} - -// Gets message hex dump information, including scrollbar information for the hex lines -// -// Parameters: -// pMessageText: The text of the message -// pRemoveTrailingCRLF: Optional boolean - Whether or not to remove any trailing CR or LF characters. -// Defaults to false. -// -// Return value: An object with the following properties: -// msgHexArray: An array of text lines representing the hex dump of the message -// topLineIdxForLastPage: For scrolling, the index of the line for the top of the last page -// msgFractionShown: For scrolling, the fraction of the lines shown -// numSolidScrollBlocks: For scrolling, the number of solid srollbar blocks to use -// numNonSolidScrollBlocks: For scrolling, the number of non-solid scrollbar blocks to use -// solidBlockStartRow: For scrolling, the row on the screen to start the scrollbar at -function DigDistMsgReader_GetMsgHexInfo(pMessageText, pRemoveTrailingCRLF) -{ - var retObj = { - msgHexArray: [], - topLineIdxForLastPage: 0, - msgFractionShown: 0.0, - numSolidScrollBlocks: 0, - numNonSolidScrollBlocks: 0, - solidBlockStartRow: 0 - }; - - var hexArray = hexdump.generate(undefined, pMessageText, /* ASCII: */true, /* offsets: */true); - if (Array.isArray(hexArray) && hexArray.length > 0) - { - if (typeof(pRemoveTrailingCRLF) === "boolean" && pRemoveTrailingCRLF) - { - // Remove the trailing CR (and possibly LF) from the last Line - hexArray[hexArray.length-1] = hexArray[hexArray.length-1].replace(/[\r\n]+$/, ""); - } - - retObj.msgHexArray = hexArray; - retObj.topLineIdxForLastPage = retObj.msgHexArray.length - this.msgAreaHeight; - if (retObj.topLineIdxForLastPage < 0) - retObj.topLineIdxForLastPage = 0; - // Variables for the scrollbar to show the fraction of the message shown - retObj.msgFractionShown = this.msgAreaHeight / retObj.msgHexArray.length; - if (retObj.msgFractionShown > 1) - retObj.msgFractionShown = 1.0; - retObj.numSolidScrollBlocks = Math.floor(this.msgAreaHeight * retObj.msgFractionShown); - if (retObj.numSolidScrollBlocks == 0) - retObj.numSolidScrollBlocks = 1; - retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks; - retObj.solidBlockStartRow = this.msgAreaTop; - } - - return retObj; -} - -// For the DDMsgReader class: Saves a message hex dump to a file on the BBS machine with the given filename -// -// Parameters: -// pMsgHdr: The header of the message -// pOutFilename: The full path & filename of the file to save the hex dump to -// -// Return value: An object with the following properties: -// saveSucceeded: Boolean - Whether or not the save succeeded -// errorMsg: A string containing an error on failure -function DigDistMsgReader_SaveMsgHexDumpToFile(pMsgHdr, pOutFilename) -{ - var retObj = { - saveSucceeded: false, - errorMsg: "" - }; - - if (typeof(pMsgHdr) !== "object" || typeof(pOutFilename) !== "string") - { - retObj.errorMsg = "Invalid parameter given"; - return retObj; - } - - var msgText = ""; - var hdrLines = null; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - msgText = msgbase.get_msg_body(false, pMsgHdr.number); - hdrLines = this.GetExtdMsgHdrInfo(msgbase, pMsgHdr.number, false, false, false, false); - msgbase.close(); - } - - var hexLines = hexdump.generate(undefined, msgText, /* ASCII: */true, /* offsets: */true); - if (Array.isArray(hexLines) && hexLines.length > 0) - { - var outFile = new File(pOutFilename); - if (outFile.open("w")) - { - // Write the message header lines - if (Array.isArray(hdrLines) && hdrLines.length > 0) - { - for (var hdrI = 0; hdrI < hdrLines.length; ++hdrI) - outFile.writeln(hdrLines[hdrI]); - } - else - { - outFile.writeln("From: " + pMsgHdr.from); - outFile.writeln("To: " + pMsgHdr.to); - outFile.writeln("Subject: " + pMsgHdr.subject); - // Message time - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr); - var dateTimeStr = ""; - if (msgWrittenLocalTime != -1) - dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime); - else - dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, ""); - outFile.writeln("Date: " + dateTimeStr); - } - outFile.writeln("================================="); - // Write the hex dump - for (var hexI = 0; hexI < hexLines.length; ++hexI) - outFile.writeln(hexLines[hexI]); - outFile.close(); - retObj.saveSucceeded = true; - } - else - retObj.errorMsg = "File write failed"; - } - else - errorMsg = "Hex dump is not available"; - - 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: An object containing the following properties: -// lastReadMsgIdx: The index of the last read message in the current message area -// lastReadMsgNum: The number of the last read message in the current message area -function DigDistMsgReader_GetLastReadMsgIdxAndNum(pMailStartFromFirst) -{ - var retObj = { - lastReadMsgIdx: -1, - lastReadMsgNum: -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) - { - retObj.lastReadMsgIdx = idx; - retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number; - } - 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) - { - retObj.lastReadMsgIdx = idx; - retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number; - break; - } - } - } - // Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if - // there is no last read message). - if (retObj.lastReadMsgIdx >= this.msgSearchHdrs[this.subBoardCode].indexed.length) - { - retObj.lastReadMsgIdx = this.msgSearchHdrs[this.subBoardCode].indexed.length - 1; - retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[retObj.lastReadMsgIdx].number; - } - } - } - else - { - //retObj.lastReadMsgIdx = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read); - retObj.lastReadMsgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read); - retObj.lastReadMsgNum = msg_area.sub[this.subBoardCode].last_read; - /* - this.hdrsForCurrentSubBoard = []; - // hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers - // to their index to hdrsForCurrentSubBoard - this.msgNumToIdxMap = {}; - */ - // Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if - // there is no last read message). - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - // If retObj.lastReadMsgIdx is -1, as a result of GetMsgIdx(), then see what the last read - // message index is according to the Synchronet message base. If - // this.hdrsForCurrentSubBoard.length has been populated, then if the last - // message index according to Synchronet is greater than that, then set the - // message index to the last index in this.hdrsForCurrentSubBoard.length. - if (retObj.lastReadMsgIdx == -1) - { - var msgIdxAccordingToMsgbase = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[this.subBoardCode].last_read); - if ((this.hdrsForCurrentSubBoard.length > 0) && (msgIdxAccordingToMsgbase >= this.hdrsForCurrentSubBoard.length)) - { - retObj.lastReadMsgIdx = this.hdrsForCurrentSubBoard.length - 1; - retObj.lastReadMsgNum = this.hdrsForCurrentSubBoard[retObj.lastReadMsgIdx].number; - } - } - //if (retObj.lastReadMsgIdx >= msgbase.total_msgs) - // retObj.lastReadMsgIdx = msgbase.total_msgs - 1; - // TODO: Is this code right? Modified 3/24/2015 to replace - // the above 2 commented lines. - if ((retObj.lastReadMsgIdx < 0) || (retObj.lastReadMsgIdx >= msgbase.total_msgs)) - { - // Look for the first message not marked as deleted - var readableMsgIdx = this.FindNextReadableMsgIdx(0, true); - // If a non-deleted message was found, then set the last read - // pointer to it. - if (readableMsgIdx > -1) - { - var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx); - 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 retObj; -} - -// 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 the user's scan pointer is a crazy value, that could be because - // the user hasn't read messages in the sub-board yet. In that case, - // just use 0. Otherwise, get the user's scan pointer message index. - var msgIdx = 0; - // If the user's scan_ptr for the sub-board isn't the 'last message' - // special value, then use it - if (!subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode)) - msgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].scan_ptr); - // Sanity checking for msgIdx - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if ((msgIdx < 0) || (msgIdx >= msgbase.total_msgs) || subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode)) - { - msgIdx = -1; - // Look for the first message not marked as deleted - var readableMsgIdx = this.FindNextReadableMsgIdx(0, true); - // If a non-deleted message was found, then set the scan pointer to it. - if (readableMsgIdx > -1) - { - var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx); - 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; - } - msgbase.close(); - } - 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 ((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.attributes = "N"; - if (pPositionCursorForStatus) - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol(); - console.gotoxy(this.msgAreaLeft, console.screen_rows); - } - console.print("\x01h\x01ySearching\x01i...\x01n"); - // 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 != null && ((nextMsgHdr.attr & MSG_DELETE) == 0) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id)) - nextMsgOffset = nextMsgHdr.offset; - } - } - */ - if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1) - { - var nextMsgHdr; - for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx) - { - nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); - if (nextMsgHdr != null && (((nextMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id)) - { - //nextMsgOffset = nextMsgHdr.offset; - nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number); - } - } - } - 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); - newMsgOffset = this.GetMsgIdx(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) || canViewDeletedMsgs()) && (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) || canViewDeletedMsgs()) && (pMsgHdr.from.toUpperCase() == fromNameUppercase)); - }; - } - else if (pThreadType == THREAD_BY_TO_USER) - { - toNameUppercase = pMsgHdr.to.toUpperCase(); - msgHdrMatch = function(pMsgHdr) { - return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.to.toUpperCase() == toNameUppercase)); - }; - } - - // Perform the search - // Write "Searching.." in case searching takes a while. - console.attributes = "N"; - if (pPositionCursorForStatus) - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol(); - console.gotoxy(this.msgAreaLeft, console.screen_rows); - } - console.print("\x01h\x01ySearching\x01i...\x01n"); - // 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 (nextMsgHdr != null && msgHdrMatch(nextMsgHdr)) - nextMsgOffset = nextMsgHdr.offset; - } - } - */ - if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1) - { - var nextMsgHdr; - for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx) - { - nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); - if (nextMsgHdr != null && msgHdrMatch(nextMsgHdr)) - { - //nextMsgOffset = nextMsgHdr.offset; - nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number); - } - } - } - 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("\x01n\x01h\x01yNo messages found.\x01n"); - 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 ((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.attributes = "N"; - if (pPositionCursorForStatus) - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol(); - console.gotoxy(this.msgAreaLeft, console.screen_rows); - } - console.print("\x01h\x01ySearching\x01i...\x01n"); - // 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 != null && ((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id)) - nextMsgOffset = prevMsgHdr.offset; - } - } - */ - if (this.GetMsgIdx(pMsgHdr.number) > 0) - { - var prevMsgHdr; - for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx) - { - prevMsgHdr = this.GetMsgHdrByIdx(messageIdx); - if (prevMsgHdr != null && (((prevMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id)) - { - //nextMsgOffset = prevMsgHdr.offset; - nextMsgOffset = this.GetMsgIdx(prevMsgHdr.number); - nextMsgOffset = 0; - } - } - } - 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); - newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_back); - if (newMsgOffset < 0) - newMsgOffset = 0; - } - - /* - // 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); - newMsgOffset = this.GetMsgIdx(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); - var firstThreadMsgIdx = this.GetMsgIdx(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.attributes = "N"; - if (pPositionCursorForStatus) - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol(); - console.gotoxy(this.msgAreaLeft, console.screen_rows); - } - console.print("\x01h\x01ySearching\x01i...\x01n"); - // 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 != null && ((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) || canViewDeletedMsgs()) && (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) || canViewDeletedMsgs()) && (pMsgHdr.from.toUpperCase() == fromNameUppercase)); - }; - } - else if (pThreadType == THREAD_BY_TO_USER) - { - toNameUppercase = pMsgHdr.to.toUpperCase(); - msgHdrMatch = function(pMsgHdr) { - return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.to.toUpperCase() == toNameUppercase)); - }; - } - - // Perform the search - // Write "Searching.." in case searching takes a while. - console.attributes = "N"; - if (pPositionCursorForStatus) - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol(); - console.gotoxy(this.msgAreaLeft, console.screen_rows); - } - console.print("\x01h\x01ySearching\x01i...\x01n"); - // 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 (prevMsgHdr != null && msgHdrMatch(nextMsgHdr)) - nextMsgOffset = nextMsgHdr.offset; - } - } - */ - if (this.GetMsgIdx(pMsgHdr.number) > 0) - { - var nextMsgHdr; - for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx) - { - nextMsgHdr = this.GetMsgHdrByIdx(messageIdx); - if (nextMsgHdr != null && msgHdrMatch(nextMsgHdr)) - { - //nextMsgOffset = nextMsgHdr.offset; - nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number); - } - } - } - 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("\x01n\x01h\x01yNo messages found.\x01n"); - 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.userSettings.listMessagesInReverse) - 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.userSettings.listMessagesInReverse) - this.lightbarListTopMsgIdx = this.NumMessages() - (this.lightbarMsgListNumLines * (pPageNum-1)) - 1; - else - { - //this.lightbarListTopMsgIdx = (this.lightbarMsgListNumLines * (pPageNum-1)); - var pageIdx = pPageNum - 1; - if (pageIdx < 0) - pageIdx = 0; - this.lightbarListTopMsgIdx = this.lightbarMsgListNumLines * pageIdx; - } -} - -// 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.userSettings.listMessagesInReverse); - this.CalcTraditionalMsgListTopIdx(newPageNum); - this.CalcLightbarMsgListTopIdx(newPageNum); - this.lightbarListSelectedMsgIdx = pMsgNum - 1; - if (this.lightbarListCurPos == null) - this.lightbarListCurPos = {}; - 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 = { - msgAreaGood: true, - 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("\x01n"); - console.gotoxy(pCurPos); - } - console.print("\x01n\x01h\x01wSearching\x01i...\x01n"); - this.msgSearchHdrs[subCode] = searchMsgbase(subCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser); - // 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 - { - // 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: Validates a message if the sub-board -// requires message validation. -// -// Parameters: -// pSubBoardCode: The internal code of the sub-board -// pMsgNum: The message number -// -// Return value: Boolean - Whether or not validating the message was successful -function DigDistMsgReader_ValidateMsg(pSubBoardCode, pMsgNum) -{ - if (!msg_area.sub[pSubBoardCode].is_moderated) - return true; - - var validationSuccessful = false; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var msgHdr = msgbase.get_msg_header(false, pMsgNum, false); - if (msgHdr != null) - { - if (!Boolean(msgHdr.attr & MSG_VALIDATED)) - { - msgHdr.attr |= MSG_VALIDATED; - validationSuccessful = msgbase.put_msg_header(false, msgHdr.number, msgHdr); - } - else - validationSuccessful = true; - } - msgbase.close(); - } - - return validationSuccessful; -} - -// For the DigDistMsgReader class: Gets the current sub-board's group name and description. -// -// Return value: An object with the following properties: -// grpName: The group name -// grpDesc: The group description -function DigDistMsgReader_GetGroupNameAndDesc() -{ - var retObj = { - grpName: "", - grpDesc: "" - } - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - retObj.grpName = msgbase.cfg.grp_name; - retObj.grpDesc = msgbase.cfg.description; - msgbase.close(); - } - return retObj; -} - -// For the DigDistMsgReader class: Lets the user manage their preferences/settings (scrollable/ANSI user interface). -// -// Parameters: -// pDrawBottomhelpLineFn: A function to draw the bottom help line (it could be the message list -// help line or reader help line), for refreshing after displaying an error. -// This function must take the reader (this) as a parameter. -// pTopRowOverride: Optional - The row on the screen for the top line of the settings dialog. -// If not specified, then 1 below the top of the message area (for reader mode) -// will be used. -// -// Return value: An object containing the following properties: -// needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be -// refreshed (i.e., when the user has edited their twitlist) -// optionBoxTopLeftX: The top-left screen column of the option box -// optionBoxTopLeftY: The top-left screen row of the option box -// optionBoxWidth: The width of the option box -// optionBoxHeight: The height of the option box -// userTwitListChanged: Boolean - Whether or not the user's personal twit list changed -function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn, pTopRowOverride) -{ - var retObj = { - needWholeScreenRefresh: false, - optionBoxTopLeftX: 1, - optionBoxTopLeftY: 1, - optionBoxWidth: 0, - optionBoxHeight: 0, - userTwitListChanged: false - }; - - if (!canDoHighASCIIAndANSI()) - { - this.DoUserSettings_Traditional(); - return retObj; - } - - // Save the user's current settings so that we can check them later to see if any - // of them changed, in order to determine whether to save the user's settings file. - var originalSettings = {}; - for (var prop in this.userSettings) - { - if (this.userSettings.hasOwnProperty(prop)) - originalSettings[prop] = this.userSettings[prop]; - } - - - // Create the user settings box - var optBoxTitle = "Setting Enabled"; - var optBoxWidth = ChoiceScrollbox_MinWidth(); - var optBoxHeight = 14; - var msgBoxTopRow = 1; - if (typeof(pTopRowOverride) === "number" && pTopRowOverride >= 1 && pTopRowOverride <= console.screen_rows - optBoxHeight + 1) - msgBoxTopRow = pTopRowOverride; - else - msgBoxTopRow = this.msgAreaTop + 1; - var optBoxStartX = this.msgAreaLeft + Math.floor((this.msgAreaWidth/2) - (optBoxWidth/2)); - if (optBoxStartX < this.msgAreaLeft) - optBoxStartX = this.msgAreaLeft; - var optionBox = new ChoiceScrollbox(optBoxStartX, msgBoxTopRow, optBoxWidth, optBoxHeight, optBoxTitle, - null/*gConfigSettings*/, false, true); - optionBox.addInputLoopExitKey(CTRL_U); - // Update the bottom help text to be more specific to the user settings box - var bottomBorderText = "\x01n\x01h\x01c" + UP_ARROW + "\x01b, \x01c" + DOWN_ARROW + "\x01b, \x01cEnter\x01y=\x01bSelect\x01n\x01c/\x01h\x01btoggle, " - + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose"; - // This one contains the page navigation keys.. Don't really need to show those, - // since the settings box only has one page right now: - /*var bottomBorderText = "\x01n\x01h\x01c"+ UP_ARROW + "\x01b, \x01c"+ DOWN_ARROW + "\x01b, \x01cN\x01y)\x01bext, \x01cP\x01y)\x01brev, " - + "\x01cF\x01y)\x01birst, \x01cL\x01y)\x01bast, \x01cEnter\x01y=\x01bSelect, " - + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose";*/ - - optionBox.setBottomBorderText(bottomBorderText, true, false); - - // Add the options to the option box - const checkIdx = 48; - const optionFormatStr = "%-" + (checkIdx-1) + "s[ ]"; - const ENH_SCROLLBAR_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Scrollbar in reader")); - if (this.userSettings.useEnhReaderScrollbar) - optionBox.chgCharInTextItem(ENH_SCROLLBAR_OPT_INDEX, checkIdx, CHECK_CHAR); - - const LIST_MESSAGES_IN_REVERSE_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "List messages in reverse")); - if (this.userSettings.listMessagesInReverse) - optionBox.chgCharInTextItem(LIST_MESSAGES_IN_REVERSE_OPT_INDEX, checkIdx, CHECK_CHAR); - - const NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX = optionBox.addTextItem(format(optionFormatStr, "Newscan: Only show new messages")); - if (this.userSettings.newscanOnlyShowNewMsgs) - optionBox.chgCharInTextItem(NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX, checkIdx, CHECK_CHAR); - - const INDEXED_MODE_NEWSCAN_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Use indexed mode for newscan")); - if (this.userSettings.useIndexedModeForNewscan) - optionBox.chgCharInTextItem(INDEXED_MODE_NEWSCAN_OPT_INDEX, checkIdx, CHECK_CHAR); - - const SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Show indexed menu if there are no new messages")); - if (this.userSettings.displayIndexedModeMenuIfNoNewMessages) - optionBox.chgCharInTextItem(SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR); - - const INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Show indexed menu after reading all new msgs")); - if (this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs) - optionBox.chgCharInTextItem(INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR); - - const INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Index menu: Snap to sub-boards w/ new messages")); - if (this.userSettings.indexedModeMenuSnapToFirstWithNew) - optionBox.chgCharInTextItem(INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR); - - const INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Index menu: Enter shows message list")); - if (this.userSettings.enterFromIndexMenuShowsMsgList) - optionBox.chgCharInTextItem(INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX, checkIdx, CHECK_CHAR); - - const READER_QUIT_TO_MSG_LIST_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Quit from reader to message list")); - if (this.userSettings.quitFromReaderGoesToMsgList) - optionBox.chgCharInTextItem(READER_QUIT_TO_MSG_LIST_OPT_INDEX, checkIdx, CHECK_CHAR); - - const PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Prompt delete after reply to personal email")); - if (this.userSettings.promptDelPersonalEmailAfterReply) - optionBox.chgCharInTextItem(PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX, checkIdx, CHECK_CHAR); - - const DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Display email 'replied' indicator")); - if (this.userSettings.displayMsgRepliedChar) - optionBox.chgCharInTextItem(DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX, checkIdx, CHECK_CHAR); - - // Create an object containing toggle values (true/false) for each option index - var optionToggles = {}; - optionToggles[ENH_SCROLLBAR_OPT_INDEX] = this.userSettings.useEnhReaderScrollbar; - optionToggles[LIST_MESSAGES_IN_REVERSE_OPT_INDEX] = this.userSettings.listMessagesInReverse; - optionToggles[NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX] = this.userSettings.newscanOnlyShowNewMsgs; - optionToggles[INDEXED_MODE_NEWSCAN_OPT_INDEX] = this.userSettings.useIndexedModeForNewscan; - optionToggles[SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX] = this.userSettings.displayIndexedModeMenuIfNoNewMessages; - optionToggles[INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX] = this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs; - optionToggles[INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX] = this.userSettings.indexedModeMenuSnapToFirstWithNew; - optionToggles[INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX] = this.userSettings.enterFromIndexMenuShowsMsgList; - optionToggles[READER_QUIT_TO_MSG_LIST_OPT_INDEX] = this.userSettings.quitFromReaderGoesToMsgList; - optionToggles[PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX] = this.userSettings.promptDelPersonalEmailAfterReply; - optionToggles[DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX] = this.userSettings.displayMsgRepliedChar; - - // Other actions - var USER_TWITLIST_OPT_INDEX = optionBox.addTextItem("Personal twit list"); - - // Set up the enter key in the box to toggle the selected item. - optionBox.readerObj = this; - optionBox.setEnterKeyOverrideFn(function(pBox) { - var itemIndex = pBox.getChosenTextItemIndex(); - if (itemIndex > -1) - { - // If there's an option for the chosen item, then update the text on the - // screen depending on whether the option is enabled or not. - if (optionToggles.hasOwnProperty(itemIndex)) - { - // Toggle the option and refresh it on the screen - optionToggles[itemIndex] = !optionToggles[itemIndex]; - if (optionToggles[itemIndex]) - optionBox.chgCharInTextItem(itemIndex, checkIdx, CHECK_CHAR); - else - optionBox.chgCharInTextItem(itemIndex, checkIdx, " "); - optionBox.refreshItemCharOnScreen(itemIndex, checkIdx); - - // Toggle the setting for the user in global user setting object. - switch (itemIndex) - { - case ENH_SCROLLBAR_OPT_INDEX: - this.readerObj.userSettings.useEnhReaderScrollbar = !this.readerObj.userSettings.useEnhReaderScrollbar; - break; - case LIST_MESSAGES_IN_REVERSE_OPT_INDEX: - this.readerObj.userSettings.listMessagesInReverse = !this.readerObj.userSettings.listMessagesInReverse; - break; - case NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX: - this.readerObj.userSettings.newscanOnlyShowNewMsgs = !this.readerObj.userSettings.newscanOnlyShowNewMsgs; - break; - case INDEXED_MODE_NEWSCAN_OPT_INDEX: - this.readerObj.userSettings.useIndexedModeForNewscan = !this.readerObj.userSettings.useIndexedModeForNewscan; - break; - case SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX: - this.readerObj.userSettings.displayIndexedModeMenuIfNoNewMessages = !this.readerObj.userSettings.displayIndexedModeMenuIfNoNewMessages; - break; - case INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX: - this.readerObj.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs = !this.readerObj.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs; - break; - case INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX: - this.readerObj.userSettings.indexedModeMenuSnapToFirstWithNew = !this.readerObj.userSettings.indexedModeMenuSnapToFirstWithNew; - break; - case INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX: - this.readerObj.userSettings.enterFromIndexMenuShowsMsgList = !this.readerObj.userSettings.enterFromIndexMenuShowsMsgList; - break; - case READER_QUIT_TO_MSG_LIST_OPT_INDEX: - this.readerObj.userSettings.quitFromReaderGoesToMsgList = !this.readerObj.userSettings.quitFromReaderGoesToMsgList; - break; - case PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX: - this.readerObj.userSettings.promptDelPersonalEmailAfterReply = !this.readerObj.userSettings.promptDelPersonalEmailAfterReply; - break; - case DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX: - this.readerObj.userSettings.displayMsgRepliedChar = !this.readerObj.userSettings.displayMsgRepliedChar; - break; - default: - break; - } - } - // For options that aren't on/off toggle options, take the appropriate action. - else - { - switch (itemIndex) - { - //case DICTIONARY_OPT_INDEX: - // break; - case USER_TWITLIST_OPT_INDEX: - console.clear("\x01n", false); - console.editfile(gUserTwitListFilename); - // Re-read the user's twitlist and see if the user's twitlist changed - var oldUserTwitList = this.readerObj.userSettings.twitList; - this.readerObj.userSettings.twitList = []; - this.readerObj.ReadUserSettingsFile(true); - retObj.userTwitListChanged = !arraysHaveSameValues(this.readerObj.userSettings.twitList, oldUserTwitList); - optionBox.continueInputLoopOverride = false; // Exit the input loop of the option box - retObj.needWholeScreenRefresh = true; - break; - default: - break; - } - } - } - }); // Option box enter key override function - - // Display the option box and have it do its input loop - var boxRetObj = optionBox.doInputLoop(true); - - // If the user changed any of their settings, then save the user settings. - // If the save fails, then output an error message. - var settingsChanged = false; - for (var prop in this.userSettings) - { - if (this.userSettings.hasOwnProperty(prop)) - { - settingsChanged = settingsChanged || (originalSettings[prop] != this.userSettings[prop]); - if (settingsChanged) - break; - } - } - if (settingsChanged) - { - if (!this.WriteUserSettingsFile()) - { - writeWithPause(1, console.screen_rows, "\x01n\x01y\x01hFailed to save settings!\x01n", ERROR_PAUSE_WAIT_MS, "\x01n", true); - // Refresh the help line - pDrawBottomhelpLineFn(this); - } - } - - optionBox.addInputLoopExitKey(CTRL_U); - - // Prepare return object values and return - retObj.optionBoxTopLeftX = optionBox.dimensions.topLeftX; - retObj.optionBoxTopLeftY = optionBox.dimensions.topLeftY; - retObj.optionBoxWidth = optionBox.dimensions.width; - retObj.optionBoxHeight = optionBox.dimensions.height; - return retObj; -} -// For the DigDistMsgReader class: Lets the user manage their preferences/settings (traditional user interface) -// -// Return value: An object containing the following properties: -// needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be -// refreshed (i.e., when the user has edited their twitlist) -// optionBoxTopLeftX: The top-left screen column of the option box -// optionBoxTopLeftY: The top-left screen row of the option box -// optionBoxWidth: The width of the option box -// optionBoxHeight: The height of the option box -// userTwitListChanged: Boolean - Whether or not the user's personal twit list changed -function DigDistMsgReader_DoUserSettings_Traditional() -{ - var retObj = { - needWholeScreenRefresh: true, - optionBoxTopLeftX: 1, - optionBoxTopLeftY: 1, - optionBoxWidth: 0, - optionBoxHeight: 0, - userTwitListChanged: false - }; - - var LIST_MESSAGES_IN_REVERSE_OPT_NUM = 1; - var NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM = 2; - var USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM = 3; - var SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM = 4; - var INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX = 5; - var INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM = 6; - var READER_QUIT_TO_MSG_LIST_OPT_NUM = 7; - var PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM = 8; - var DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM = 9; - var USER_TWITLIST_OPT_NUM = 10; - var HIGHEST_CHOICE_NUM = USER_TWITLIST_OPT_NUM; - - console.crlf(); - var wordFirstCharAttrs = "\x01c\x01h"; - var wordRemainingAttrs = "\x01c"; - console.print(colorFirstCharAndRemainingCharsInWords("User Settings", wordFirstCharAttrs, wordRemainingAttrs) + "\r\n"); - printTradUserSettingOption(LIST_MESSAGES_IN_REVERSE_OPT_NUM, "List messages in reverse", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM, "Only show new messages for newscan", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM, "Use Indexed mode for newscan", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM, "Show indexed menu if there are no new messages", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX, "Show indexed menu after reading all new messages", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM, "Index: Selection shows message list", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(READER_QUIT_TO_MSG_LIST_OPT_NUM, "Quitting From reader goes to message list", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM, "Prompt to delete personal message after replying", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM, "Display email replied indicator", wordFirstCharAttrs, wordRemainingAttrs); - printTradUserSettingOption(USER_TWITLIST_OPT_NUM, "Personal twit list", wordFirstCharAttrs, wordRemainingAttrs); - console.crlf(); - console.print("\x01cYour choice (\x01hQ\x01n\x01c: Quit)\x01h: \x01g"); - var userChoiceNum = console.getnum(HIGHEST_CHOICE_NUM); - console.attributes = "N"; - var userChoiceStr = userChoiceNum.toString().toUpperCase(); - if (userChoiceStr.length == 0 || userChoiceStr == "Q") - return retObj; - - var userSettingsChanged = false; - switch (userChoiceNum) - { - case LIST_MESSAGES_IN_REVERSE_OPT_NUM: - var oldListMsgsInReverseSetting = this.userSettings.listMessagesInReverse; - this.userSettings.listMessagesInReverse = !console.noyes("List messages in reverse"); - userSettingsChanged = (this.userSettings.listMessagesInReverse != oldListMsgsInReverseSetting); - break; - case NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM: - var oldOnlyShowNewMsgsSetting = this.userSettings.newscanOnlyShowNewMsgs; - this.userSettings.newscanOnlyShowNewMsgs = !console.noyes("Only show new messages for newscan"); - userSettingsChanged = (this.userSettings.newscanOnlyShowNewMsgs != oldOnlyShowNewMsgsSetting); - break; - case USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM: - var oldIndexedModeNewscanSetting = this.userSettings.useIndexedModeForNewscan; - this.userSettings.useIndexedModeForNewscan = !console.noyes("Use indexed mode for newscan-all"); - userSettingsChanged = (this.userSettings.useIndexedModeForNewscan != oldIndexedModeNewscanSetting); - break; - case SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM: - var oldIndexedMenuIfNoMsgsSetting = this.userSettings.displayIndexedModeMenuIfNoNewMessages; - this.userSettings.displayIndexedModeMenuIfNoNewMessages = !console.noyes("Show indexed menu if there are no new messages"); - userSettingsChanged = (this.userSettings.displayIndexedModeMenuIfNoNewMessages != oldIndexedMenuIfNoMsgsSetting); - break; - case INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX: - var oldIndexedMenuAfterReadingAllNewMsgsSetting = this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs; - this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs = console.yesno("Show indexed menu after reading all new messages"); - userSettingsChanged = (this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs != oldIndexedMenuAfterReadingAllNewMsgsSetting); - break; - case INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM: - var oldIndexedModeEnterShowsMsgListSetting = this.userSettings.enterFromIndexMenuShowsMsgList; - this.userSettings.enterFromIndexMenuShowsMsgList = !console.noyes("Index menu: Show message list with enter"); - userSettingsChanged = (this.userSettings.enterFromIndexMenuShowsMsgList != oldIndexedModeEnterShowsMsgListSetting); - break; - case READER_QUIT_TO_MSG_LIST_OPT_NUM: - var oldReaderQuitSetting = this.userSettings.quitFromReaderGoesToMsgList; - this.userSettings.quitFromReaderGoesToMsgList = !console.noyes("Quit key from reader: Go to the message list (rather than exit)"); - userSettingsChanged = (this.userSettings.quitFromReaderGoesToMsgList != oldReaderQuitSetting); - break; - case PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM: - var oldReaderQuitSetting = this.userSettings.promptDelPersonalEmailAfterReply; - this.userSettings.promptDelPersonalEmailAfterReply = !console.noyes("Prompt to delete personal message after replying"); - userSettingsChanged = (this.userSettings.promptDelPersonalEmailAfterReply != oldReaderQuitSetting); - break; - case DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM: - var oldDisplayRepliedCharSetting = this.userSettings.displayMsgRepliedChar; - this.userSettings.displayMsgRepliedChar = console.yesno("Display email 'replied' indicator"); - userSettingsChanged = (this.userSettings.displayMsgRepliedChar != oldDisplayRepliedCharSetting); - break; - case USER_TWITLIST_OPT_NUM: - console.editfile(gUserTwitListFilename); - // Re-read the user's twitlist and see if the user's twitlist changed - var oldUserTwitList = this.userSettings.twitList; - this.userSettings.twitList = []; - this.ReadUserSettingsFile(true); - retObj.userTwitListChanged = !arraysHaveSameValues(this.userSettings.twitList, oldUserTwitList); - retObj.needWholeScreenRefresh = true; - break; - } - - // If any user settings changed, then write them to the user settings file - if (userSettingsChanged) - { - if (!this.WriteUserSettingsFile()) - { - console.print("\x01n\r\n\x01y\x01hFailed to save settings!\x01n"); - console.crlf(); - console.pause(); - } - } - - return retObj; -} -// Helper for DigDistMsgReader_DoUserSettings_Traditional: Returns a string where for each word, -// the first letter will have one set of Synchronet attributes applied and the remainder of the word -// will have another set of Synchronet attributes applied -function colorFirstCharAndRemainingCharsInWords(pStr, pWordFirstCharAttrs, pWordRemainderAttrs) -{ - if (typeof(pStr) !== "string" || pStr.length == 0) - return ""; - if (typeof(pWordFirstCharAttrs) !== "string" || typeof(pWordRemainderAttrs) !== "string") - return pStr; - - var wordsArray = pStr.split(" "); - for (var i = 0; i < wordsArray.length; ++i) - { - if (wordsArray[i] != " ") - wordsArray[i] = "\x01n" + pWordFirstCharAttrs + wordsArray[i].substr(0, 1) + "\x01n" + pWordRemainderAttrs + wordsArray[i].substr(1); - } - return wordsArray.join(" "); -} - -// Helper for DigDistMsgReader_DoUserSettings_Traditional: Returns a string where for each word, -// the first letter will have one set of Synchronet attributes applied and the remainder of the word -// will have another set of Synchronet attributes applied -function printTradUserSettingOption(pOptNum, pStr, pWordFirstCharAttrs, pWordRemainderAttrs) -{ - printf("\x01c\x01h%d\x01g: %s\r\n", pOptNum, colorFirstCharAndRemainingCharsInWords(pStr, pWordFirstCharAttrs, pWordRemainderAttrs)); -} - -/////////////////////////////////////////////////////////////// -// Stuff for indexed reader mode - -// For the DigDistMsgReader class: Starts indexed mode. For a new scan, displays -// a menu of sub-boards with the number of new messages etc. -// -// Parameters: -// pScanScope: Numeric - Whether to scan the current sub-board, group, or all. -// This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL. -// pNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration. -// Defaults to false. -function DigDistMsgReader_DoIndexedMode(pScanScope, pNewscanOnly) -{ - var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL); - var newscanOnly = (typeof(pNewscanOnly) === "boolean" ? pNewscanOnly : false); - - this.indexedMode = true; - this.doingMsgScan = newscanOnly; - - // msgHdrsCache is used to prevent long loading again when loading a sub-board - // a 2nd time or later. Only if this.enableIndexedModeMsgListCache is true. - var msgHdrsCache = {}; - - var clearScreenForMenu = true; - var drawMenu = true; - var writeBottomHelpLine = true; - var continueOn = true; - while (continueOn) - { - // A backup for the number of new messages so we can see if it changes (due to the user reading messages) - var origNumNewMessages = 0; - // Let the user choose a sub-board, and if their choice is valid, - // let them read the sub-board. - var indexRetObj = this.IndexedModeChooseSubBoard(clearScreenForMenu, drawMenu, writeBottomHelpLine, pScanScope, pNewscanOnly); - if (typeof(indexRetObj.numNewMsgs) === "number") - origNumNewMessages = indexRetObj.numNewMsgs; - var userChoseAValidSubBoard = (typeof(indexRetObj.chosenSubCode) === "string" && msg_area.sub.hasOwnProperty(indexRetObj.chosenSubCode)); - if (userChoseAValidSubBoard) - { - // Ensure we draw the index menu & help line in the next iteration - drawMenu = true; - writeBottomHelpLine = true; - this.subBoardCode = indexRetObj.chosenSubCode; - if (!this.enableIndexedModeMsgListCache || !msgHdrsCache.hasOwnProperty(indexRetObj.chosenSubCode)) - { - // Display a "Loading..." status text and populate the headers for this sub-board - if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // this.msgListUseLightbarListInterface - { - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.print("\x01n\x01cLoading\x01h...\x01n"); - } - // If the user has the option to only show new messages for a newscan and there are - // new messages, then populate the sub-board with only the new message from their - // scan pointer; otherwise, populate with all message headers for the sub-board. - if (this.userSettings.newscanOnlyShowNewMsgs && indexRetObj.numNewMsgs > 0) - this.PopulateHdrsForCurrentSubBoard(POPULATE_MSG_HDRS_FROM_SCAN_PTR); - else - this.PopulateHdrsForCurrentSubBoard(POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS); - } - else - { - this.hdrsForCurrentSubBoard = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoard; - this.msgNumToIdxMap = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoardByMsgNum; - } - - // If the user chose to view the message list, display the message list to let the user - // choose a message to read. Otherwise, start reader mode. - if (indexRetObj.viewMsgList) - { - var listRetObj = this.ListMessages(indexRetObj.chosenSubCode, false); - // If the user chose a message from the list, let the user read starting with that message - if (listRetObj.selectedMsgOffset > -1) - { - var readRetObj = this.ReadMessages(indexRetObj.chosenSubCode, listRetObj.selectedMsgOffset, false, false, true, false); - // Update the text for the current menu item to ensure the message numbers are up to date - var currentMenuItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx); - var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(indexRetObj.chosenSubCode); - currentMenuItem.text = itemInfo.itemText; - currentMenuItem.retval.numNewMsgs = itemInfo.numNewMsgs; - // If enabled, store the message headers for this sub-board as a cache for performance - if (this.enableIndexedModeMsgListCache) - { - msgHdrsCache[indexRetObj.chosenSubCode] = { - hdrsForCurrentSubBoard: this.hdrsForCurrentSubBoard, - hdrsForCurrentSubBoardByMsgNum: this.msgNumToIdxMap - }; - } - } - } - else - { - // Let the user read the sub-board - // Decide the index of the starting message: If there are no new messages, show the last - // messages. Otherwise, if only showing new messages, show the first messages. - // Otherwise, calculate the starting index. - var numMessages = this.NumMessages(); - var startIdx = 0; - if (newscanOnly) - { - if (indexRetObj.numNewMsgs == 0) - startIdx = numMessages - 1; - else if (!this.userSettings.newscanOnlyShowNewMsgs) - { - startIdx = numMessages > 0 ? numMessages - indexRetObj.numNewMsgs : 0; - if (startIdx < 0) - startIdx = numMessages - 1; - } - } - else - { - // Not a newscan - Use the last_read pointer - var tmpMsgbase = new MsgBase(indexRetObj.chosenSubCode); - if (tmpMsgbase.open()) - { - var lastReadMsgHdr = tmpMsgbase.get_msg_index(false, msg_area.sub[indexRetObj.chosenSubCode].last_read, false); - if (lastReadMsgHdr != null) - { - //startIdx = absMsgNumToIdxWithMsgbaseObj(tmpMsgbase, lastReadMsgHdr.number); - - //this.PopulateHdrsForCurrentSubBoard(); - this.subBoardCode = indexRetObj.chosenSubCode; - if (this.hdrsForCurrentSubBoard.length > 0) - { - startIdx = this.GetMsgIdx(GetScanPtrOrLastMsgNum(this.subBoardCode)) + 1; - if (startIdx < 0) - startIdx = 0; - else if (startIdx >= this.hdrsForCurrentSubBoard.length) - startIdx = this.hdrsForCurrentSubBoard.length - 1; - } - } - tmpMsgbase.close(); - } - } - - // pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList, pAllowChgArea, pReturnOnNextAreaNav, - // pPromptToGoToNextAreaIfNoSearchResults - var readRetObj = this.ReadMessages(indexRetObj.chosenSubCode, startIdx, false, false, true, false); - // Even if not doing a newscan, still update the scan pointer to the user's last_read pointer - if (!newscanOnly) - { - if (typeof(msg_area.sub[indexRetObj.chosenSubCode].scan_ptr) === "number") - { - if (!msgNumIsLatestMsgSpecialVal(msg_area.sub[indexRetObj.chosenSubCode].scan_ptr) && msg_area.sub[indexRetObj.chosenSubCode].scan_ptr < msg_area.sub[indexRetObj.chosenSubCode].last_read) - msg_area.sub[indexRetObj.chosenSubCode].scan_ptr = msg_area.sub[indexRetObj.chosenSubCode].last_read; - } - else - msg_area.sub[indexRetObj.chosenSubCode].scan_ptr = msg_area.sub[indexRetObj.chosenSubCode].last_read; - } - - // Update the text for the current menu item to ensure the message numbers are up to date - var currentMenuItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx); - var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(indexRetObj.chosenSubCode); - currentMenuItem.text = itemInfo.itemText; - currentMenuItem.retval.numNewMsgs = itemInfo.numNewMsgs; - // If enabled, store the message headers for this sub-board as a cache for performance - if (this.enableIndexedModeMsgListCache) - { - msgHdrsCache[indexRetObj.chosenSubCode] = { - hdrsForCurrentSubBoard: this.hdrsForCurrentSubBoard, - hdrsForCurrentSubBoardByMsgNum: this.msgNumToIdxMap - }; - } - if (!readRetObj.stoppedReading && (readRetObj.lastAction == ACTION_GO_NEXT_MSG_AREA || readRetObj.lastAction == ACTION_GO_NEXT_MSG)) - this.indexedModeSetIdxMnuIdxOneMore = true; - else - this.indexedModeSetIdxMnuIdxOneMore = false; - - /* - switch (readRetObj.lastAction) - { - case ACTION_QUIT: - //continueOn = false; - break; - default: - break; - } - */ - } - - // If the number of new messages has changed (due to reading the sub-board), - // then empty the header caches so that we'll fully populate them next time - // the user chooses the same sub-board - var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(indexRetObj.chosenSubCode); - if (latestPostInfo.numNewMsgs != origNumNewMessages) - { - this.hdrsForCurrentSubBoard = []; - this.msgNumToIdxMap = {}; - if (msgHdrsCache.hasOwnProperty(indexRetObj.chosenSubCode)) - delete msgHdrsCache[indexRetObj.chosenSubCode]; - } - } - else - { - // On ? keypress, show the help screen. Otherwise, quit. - if (indexRetObj.lastUserInput == this.indexedModeMenuKeys.help) - { - this.ShowIndexedListHelp(); - drawMenu = true; - clearScreenForMenu = true; - writeBottomHelpLine = true; - } - // Ctrl-U: User settings - else if (indexRetObj.lastUserInput == this.indexedModeMenuKeys.userSettings) - { - drawMenu = false; - clearScreenForMenu = false; - writeBottomHelpLine = false; - if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // pReader.msgListUseLightbarListInterface - { - var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { - var usingANSI = console.term_supports(USER_ANSI) && pReader.indexedModeMenu.allowANSI; // pReader.msgListUseLightbarListInterface - if (usingANSI) - { - // Make sure the help line is built, if not already - pReader.MakeIndexedModeHelpLine(); - // Display the help line at the bottom of the screen - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.putmsg(pReader.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking - console.attributes = "N"; - } - }, 2); - if (userSettingsRetObj.needWholeScreenRefresh) - { - drawMenu = true; - clearScreenForMenu = true; - writeBottomHelpLine = true; - } - else - { - this.indexedModeMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX, - userSettingsRetObj.optionBoxTopLeftY, - userSettingsRetObj.optionBoxWidth, - userSettingsRetObj.optionBoxHeight); - } - } - else - this.DoUserSettings_Traditional(); - } - else - continueOn = false; - } - } - - this.indexedMode = false; - this.doingMsgScan = false; -} - -// For indexed mode: Displays any sub-boards with new messages and lets the user choose one -// -// Parameters: -// pClearScreen: Whether or not to clear the screen. Defaults to true. -// pDrawMenu: Whether or not to draw the menu. Defaults to true. -// pDisplayHelpLine: Whether or not to draw the help line at the bottom of the screen. Defaults to true. -// pScanScope: Numeric - Whether to scan the current sub-board, group, or all. -// This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL. -// pNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration. -// Defaults to false. -// -// Return value: An object containing the following values: -// chosenSubCode: The user's chosen sub-board code; if none selected, this will be null -// numNewMsgs: The number of new messages in the chosen sub-board -// viewMsgList: Whether or not to view the message list instead of going to reader mode -// lastUserInput: The last keypress entered by the user -function DigDistMsgReader_IndexedModeChooseSubBoard(pClearScreen, pDrawMenu, pDisplayHelpLine, pScanScope, pNewscanOnly) -{ - var retObj = { - chosenSubCode: null, - numNewMsgs: 0, - viewMsgList: false, // To view the message list instead of reader mode - lastUserInput: "" - }; - - var clearScreen = (typeof(pClearScreen) === "boolean" ? pClearScreen : true); - var displayHelpLine = (typeof(pDisplayHelpLine) === "boolean" ? pDisplayHelpLine : true); - - var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL); - var newScanOnly = (typeof(pNewscanOnly) === "boolean" ? pNewscanOnly : false); - - // Note: DDlightbarMenu supports non-ANSI terminals with a more traditional UI - // of listing the items and letting the user choose one by typing its number. - // If we are to use the traditional interface, the menu will be in numbered mode, - // so we'll need to account for the width of the number of items in the menu. - var usingTradInterface = !this.msgListUseLightbarListInterface || !console.term_supports(USER_ANSI); - var numberedModeItemNumWidth = 0; - if (usingTradInterface) - { - // Count the number of items that we'll add to the menu - var numItems = 0; - for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) - { - // If scanning the user's current group or sub-board and this is the wrong group, then skip this group. - if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx) - continue; - - var grpNameItemAddedToMenu = false; - for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx) - { - // Skip sub-boards that the user can't read or doesn't have configured for newscans - if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read) - continue; - if ((msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW) == 0) - continue; - // If scanning the user's current sub-board and this is the wrong sub-board, then - // skip this sub-board (the other groups should have been skipped in the outer loop). - if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx) - continue; - // Count the item for the group separator (if not added), as well as the item itself - if (!grpNameItemAddedToMenu) - { - ++numItems; - grpNameItemAddedToMenu = true; - } - ++numItems; - } - } - if (numItems > 0) - numberedModeItemNumWidth = numItems.toString().length; - } - - // Set text widths for the menu items - var newMsgWidthObj = findWidestNumMsgsAndNumNewMsgs(scanScope, newScanOnly); - var numMsgsWidth = newMsgWidthObj.widestNumMsgs; - var numNewMsgsWidth = newMsgWidthObj.widestNumNewMsgs; - // Ensure the column widths for the last few columns (after description) are wide enough - // to fit the labels - if (numMsgsWidth < 5) // "Total" - numMsgsWidth = 5; - if (numNewMsgsWidth < 3) // "New" - numNewMsgsWidth = 3; - var lastPostDateWidth = 10; - this.indexedModeItemDescWidth = console.screen_columns - numMsgsWidth - numNewMsgsWidth - lastPostDateWidth - 4; - if (usingTradInterface) - this.indexedModeItemDescWidth -= (numberedModeItemNumWidth+1); - this.indexedModeSubBoardMenuSubBoardFormatStr = "%-" + this.indexedModeItemDescWidth + "s %" + numMsgsWidth + "d %" + numNewMsgsWidth + "d %" + lastPostDateWidth + "s"; - var thisFunctionFirstCall = false; // Whether or not this is the first call of this function - if (typeof(this.indexedModeMenu) !== "object") - { - thisFunctionFirstCall = true; - this.indexedModeMenu = this.CreateLightbarIndexedModeMenu(numMsgsWidth, numNewMsgsWidth, lastPostDateWidth, this.indexedModeItemDescWidth, this.indexedModeSubBoardMenuSubBoardFormatStr); - } - else - { - DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = this.indexedModeMenu.selectedItemIdx; - /* - // Temporary - if (user.is_sysop) - { - console.print("\x01n\r\n"); - console.print("Indexed menu item index: " + DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx + "\r\n"); - console.pause(); - } - // End Temporary - */ - } - // Ensure the menu is clear, and (re-)populate the menu with sub-board information w/ # of new messages in each, etc. - // Also, build an array of sub-board codes for each menu item. - this.indexedModeMenu.RemoveAllItems(); - var numSubBoards = 0; - var totalNewMsgs = 0; - for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) - { - // If scanning the user's current group or sub-board and this is the wrong group, then skip this group. - if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx) - continue; - - var grpNameItemAddedToMenu = false; - for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx) - { - // Skip sub-boards that the user can't read or doesn't have configured for newscans - if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read) - continue; - if (newScanOnly && !Boolean(msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW)) - continue; - // If scanning the user's current sub-board and this is the wrong sub-board, then - // skip this sub-board (the other groups should have been skipped in the outer loop). - if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx) - continue; - - ++numSubBoards; - - if (!grpNameItemAddedToMenu) - { - //var grpDesc = msg_area.grp_list[grpIdx].name + " - " + msg_area.grp_list[grpIdx].description; - var grpDesc = msg_area.grp_list[grpIdx].name; - if (msg_area.grp_list[grpIdx].name != msg_area.grp_list[grpIdx].description) - grpDesc += " - " + msg_area.grp_list[grpIdx].description; - var menuItemText = "\x01n" + this.colors.indexMenuSeparatorLine + charStr(HORIZONTAL_SINGLE, 5); - menuItemText += "\x01n" + this.colors.indexMenuSeparatorText + " "; - menuItemText += grpDesc; - var menuItemLen = console.strlen(menuItemText); - if (menuItemLen < this.indexedModeMenu.size.width) - { - menuItemText += " \x01n" + this.colors.indexMenuSeparatorLine; - var numChars = this.indexedModeMenu.size.width - menuItemLen - 1; - menuItemText += charStr(HORIZONTAL_SINGLE, numChars); - } - menuItemText = skipsp(truncsp(menuItemText)); // Trim leading & trailing whitespace - this.indexedModeMenu.Add(menuItemText, null, null, false); // Not selectable - grpNameItemAddedToMenu = true; - } - - var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(msg_area.grp_list[grpIdx].sub_list[subIdx].code); - this.indexedModeMenu.Add(itemInfo.itemText, { - subCode: msg_area.grp_list[grpIdx].sub_list[subIdx].code, - numNewMsgs: itemInfo.numNewMsgs - }); - - totalNewMsgs += itemInfo.numNewMsgs; - } - } - // If there are no items on the menu, then show a message and return - if (this.indexedModeMenu.NumItems() == 0) - { - if (newScanOnly) - { - console.print("\x01n" + this.text.msgScanCompleteText + "\x01n"); - console.crlf(); - console.pause(); - } - return retObj; - } - // For a newscan, if there are no new messages and the user setting to show the indexed menu when there are no new messages - // is disabled, then say so and return. - else if (thisFunctionFirstCall && totalNewMsgs == 0 && (!this.userSettings.displayIndexedModeMenuIfNoNewMessages || !this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs)) - { - if (newScanOnly) - { - console.attributes = "N"; - console.putmsg(bbs.text(QWKNoNewMessages)); - //console.crlf(); - console.pause(); - return retObj; - } - } - // For a newscan, if this is not the first time the indexed newscan menu is being displayed and there are no more new - // messages, and the user has the setting to show the indexed newscan menu now is disabled, then return. - else if (!thisFunctionFirstCall && totalNewMsgs == 0 && !this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs) - { - if (newScanOnly) - { - console.attributes = "N"; - console.crlf(); - printf(bbs.text(MessageScanComplete), numSubBoards); - console.pause(); - return retObj; - } - } - - // If we've saved the index of the selected item in the menu, then set it back in the menu, if it's - // valid. This is done because the list of items is cleared each time this function is called. - if (!thisFunctionFirstCall && typeof(DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx) === "number") - { - var savedItemIdx = DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx; - if (this.indexedModeSetIdxMnuIdxOneMore) - ++savedItemIdx; - if (savedItemIdx >= 0 && savedItemIdx < this.indexedModeMenu.NumItems()) - setIndexedSubBoardMenuSelectedItemIdx(this.indexedModeMenu, savedItemIdx); - else - DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = 0; - } - - // For a newscan, if the user setting to "snap" to the first sub-board with new messages is enabled, - // then set that as the selected item index. - if (newScanOnly && this.userSettings.indexedModeMenuSnapToFirstWithNew) - { - var foundMenuItem = indexedSubMenuSetSelectedNextWithnNewMsgs(this.indexedModeMenu, this.indexedModeMenu.selectedItemIdx, this.indexedModeMenu.NumItems()); - // If we haven't found a sub-board with new messages and we didn't start at the - // first, then wrap around - if (!foundMenuItem && this.indexedModeMenu.selectedItemIdx > 0) - indexedSubMenuSetSelectedNextWithnNewMsgs(this.indexedModeMenu, 0, this.indexedModeMenu.selectedItemIdx); - } - - // Clear the screen, if desired - if (clearScreen) - console.clear("\x01n"); - - // If using ANSI, then display the help line at the bottom of the scren - var usingANSI = console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI; // this.msgListUseLightbarListInterface - if (usingANSI && displayHelpLine) - { - // Make sure the help line is built, if not already - this.MakeIndexedModeHelpLine(); - // Display the help line at the bottom of the screen - console.gotoxy(1, console.screen_rows); - console.attributes = "N"; - console.putmsg(this.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking - console.attributes = "N"; - } - - // Output a header above the menu - if (usingANSI) - console.gotoxy(1, 1); - var descWidthForHdr = this.indexedModeItemDescWidth; - var maxScreenWidth = console.screen_columns - 3; // 3 spaces - var currentTotalColWidth = descWidthForHdr + numMsgsWidth + numNewMsgsWidth + lastPostDateWidth; - if (currentTotalColWidth < maxScreenWidth) - descWidthForHdr += (maxScreenWidth - currentTotalColWidth - 1); - var indexMenuHdrFormatStr = "\x01n" + this.colors.indexMenuHeader; - indexMenuHdrFormatStr += "%-" + descWidthForHdr + "s %" + numMsgsWidth + "s %" + numNewMsgsWidth + "s %" + lastPostDateWidth + "s"; - printf(indexMenuHdrFormatStr, "Description", "Total", "New", "Last Post"); - console.attributes = "N"; - if (!usingANSI) - console.crlf(); - - // Indexed mode menu input loop - var continueOn = true; - var drawMenu = (typeof(pDrawMenu) === "boolean" ? pDrawMenu : true); - while (continueOn) - { - var menuRetval = this.indexedModeMenu.GetVal(drawMenu); - // Show the menu and get the user's choice - retObj.lastUserInput = this.indexedModeMenu.lastUserInput; - var lastUserInputUpper = ""; - if (typeof(this.indexedModeMenu.lastUserInput) === "string") - lastUserInputUpper = this.indexedModeMenu.lastUserInput.toUpperCase(); - if (menuRetval != null) - { - retObj.chosenSubCode = menuRetval.subCode; - retObj.numNewMsgs = menuRetval.numNewMsgs; - // If the user has the option enabled to view the message list when pressing enter here, - // then allow that. - if (this.userSettings.enterFromIndexMenuShowsMsgList) - retObj.viewMsgList = true; - continueOn = false; - } - else if (lastUserInputUpper == this.indexedModeMenuKeys.quit || retObj.lastUserInput == KEY_ESC) - continueOn = false; - else if (lastUserInputUpper == this.indexedModeMenuKeys.showMsgList) - { - // Message list for the highlighted sub-board - var highlightedItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx); - retObj.chosenSubCode = highlightedItem.retval.subCode; - retObj.numNewMsgs = highlightedItem.retval.numNewMsgs; - retObj.viewMsgList = true; - continueOn = false; - } - else if (lastUserInputUpper == this.indexedModeMenuKeys.markAllRead) - { - // Mark all read in the sub-board - var highlightedItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx); - if (subBoardNewscanAllRead(highlightedItem.retval.subCode)) - { - // Update the item in the indexed mode menu - var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(highlightedItem.retval.subCode); - var menuItem = this.indexedModeMenu.MakeItemWithRetval({ - subCode: highlightedItem.retval.subCode, - numNewMsgs: itemInfo.numNewMsgs - }); - menuItem.text = itemInfo.itemText; - this.indexedModeMenu.items[this.indexedModeMenu.selectedItemIdx] = menuItem; - this.indexedModeMenu.WriteItemAtItsLocation(this.indexedModeMenu.selectedItemIdx, true, false); - } - drawMenu = false; // No need to re-draw the whole menu - } - else if (lastUserInputUpper == this.indexedModeMenuKeys.userSettings) - { - // The calling function will do the user settings dialog - continueOn = false; - retObj.lastUserInput = this.indexedModeMenuKeys.userSettings; - } - else if (lastUserInputUpper == this.indexedModeMenuKeys.help) - { - // The calling function will show the help screen and re-drawe - // the bottom help line below the menu - continueOn = false; - retObj.lastUserInput = this.indexedModeMenuKeys.help; - } - } - console.attributes = "N"; - return retObj; -} - -// Helper for DigDistMsgReader_IndexedModeChooseSubBoard(): Sets the selected item in the -// indexed mode sub-board menu and adjusts the menu items to be in a good location -// -// Parameters: -// pIndexSubBoardMenu: The indexed sub-board menu -// pSelectedItemIdx: The index of the item to set as the selected item -function setIndexedSubBoardMenuSelectedItemIdx(pIndexSubBoardMenu, pSelectedItemIdx) -{ - if (pSelectedItemIdx >= 0 && pSelectedItemIdx < pIndexSubBoardMenu.NumItems()) - { - pIndexSubBoardMenu.SetSelectedItemIdx(pSelectedItemIdx); - // If the indexed menu has more items than will fit on the screen & isn't on the last - // page, then set the top item index to one before the selected index (if >0) or the - // same as the selected item index. - var selectedItemIsFirst = pIndexSubBoardMenu.selectedItemIdx == pIndexSubBoardMenu.topItemIdx; - var selectedItemOnLastPage = pSelectedItemIdx >= pIndexSubBoardMenu.GetTopItemIdxOfLastPage(); - var moreThanOneScreenfulOfItems = pIndexSubBoardMenu.NumItems() > console.screen_columns - 2; - // Checking if the selected item index is on the first page - var numItems = pIndexSubBoardMenu.NumItems(); - var numItemsPerPage = pIndexSubBoardMenu.GetNumItemsPerPage(); - var lastItemIdxForFirstPage = (numItems > numItemsPerPage ? numItemsPerPage - 1 : numItems - 1); - var selectedItemIsOnFirstPage = (pIndexSubBoardMenu.selectedItemIdx >= 0 && pIndexSubBoardMenu.selectedItemIdx <= lastItemIdxForFirstPage); - if (!selectedItemIsFirst && !selectedItemOnLastPage && moreThanOneScreenfulOfItems && !selectedItemIsOnFirstPage) - { - if (pIndexSubBoardMenu.selectedItemIdx > 0) - pIndexSubBoardMenu.topItemIdx = pIndexSubBoardMenu.selectedItemIdx - 1; - else - pIndexSubBoardMenu.topItemIdx = pIndexSubBoardMenu.selectedItemIdx; - } - } -} - -// Helper for DigDistMsgReader_IndexedModeChooseSubBoard(): Sets the selected item in the -// indexed mode sub-board menu to the next sub-board with new messages, including the -// one at the given starting index. -// -// Parameters: -// pIndexSubBoardMenu: The indexed sub-board menu -// pStartIdx: The index of the sub-board to start at -// pOnePastLastIdx: One past the index of the last sub-board to check -// -// Return value: Boolean - Whether or not a sub-board with new messages was found in the menu -function indexedSubMenuSetSelectedNextWithnNewMsgs(pIndexSubBoardMenu, pStartIdx, pOnePastLastIdx) -{ - var foundMenuItem = false; - for (var i = pStartIdx; i < pOnePastLastIdx; ++i) - { - var menuItem = pIndexSubBoardMenu.GetItem(i); - if (menuItem == null || typeof(menuItem) !== "object" || !menuItem.hasOwnProperty("retval")) - continue; - if (menuItem.retval != null && menuItem.retval.numNewMsgs > 0) - { - if (menuItem.retval.numNewMsgs > 0) - { - setIndexedSubBoardMenuSelectedItemIdx(pIndexSubBoardMenu, i); - DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = i; - foundMenuItem = true; - break; - } - } - } - return foundMenuItem; -} - -// Returns a string to use for a sub-board for the indexed mode sub-board menu -// -// Parameters: -// pSubCode: The internal code of the sub-board -// -// Return value: An object containing the following properties: -// itemText: A string for the indexed mode menu item for the sub-board -// numNewMsgs: The number of new messages in the sub-board -function DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo(pSubCode) -{ - var retObj = { - itemText: "", - numNewMsgs: 0 - }; - - if (typeof(this.indexedModeSubBoardMenuSubBoardFormatStr) !== "string" || typeof(this.indexedModeItemDescWidth) !== "number") - return retObj; - - // posts: number of messages currently posted to this sub-board (introduced in v3.18c) - var totalNumMsgsInSub = msg_area.sub[pSubCode].posts; - var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(pSubCode); - var lastPostDate = strftime("%Y-%m-%d", latestPostInfo.latestMsgTimestamp); - var subDesc = (latestPostInfo.numNewMsgs > 0 ? "NEW " : " "); - subDesc += msg_area.sub[pSubCode].name; - if (msg_area.sub[pSubCode].name !== msg_area.sub[pSubCode].description) - subDesc += " - " + msg_area.sub[pSubCode].description; - subDesc = subDesc.substr(0, this.indexedModeItemDescWidth); - retObj.itemText = format(this.indexedModeSubBoardMenuSubBoardFormatStr, subDesc, totalNumMsgsInSub, latestPostInfo.numNewMsgs, lastPostDate); - retObj.numNewMsgs = latestPostInfo.numNewMsgs; - return retObj; -} - -// Builds the indexed mode help line (for the bottom of the screen) if it's not already -// built yet -function DigDistMsgReader_MakeIndexedModeHelpLine() -{ - // If it's already built, then just return now - if (typeof(this.indexedModeHelpLine) === "string") - return; - - this.indexedModeHelpLine = this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/" - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`P`" + "\x1b[V" + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`PgDn`" + KEY_PAGEDN + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/" - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`N`" + "\x1b[V" + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`ENTER`" + KEY_ENTER + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/" - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`F`" + "\x1b[V" + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`END`" + KEY_END + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/" - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`L`" + "\x1b[V" + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Ctrl-U`" + CTRL_U + "@" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Q`Q@" - + this.colors.lightbarIndexedModeHelpLineParenColor + ")" - + this.colors.lightbarIndexedModeHelpLineGeneralColor + "uit, " - + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`?`?@"; - // Add spaces so that the text is centered on the screen - var helpLineLen = 60; - var leftSideNumChars = Math.floor(console.screen_columns / 2) - Math.floor(helpLineLen / 2); - var rightSideNumChars = leftSideNumChars; - var totalNumChars = leftSideNumChars + rightSideNumChars + helpLineLen; - var maxLen = console.screen_columns - 1; - if (totalNumChars > maxLen) - rightSideNumChars -= (totalNumChars - maxLen); - else if (totalNumChars < maxLen) - rightSideNumChars += (maxLen - totalNumChars); - this.indexedModeHelpLine = "\x01n" + this.colors.lightbarIndexedModeHelpLineBkgColor + format("%*s", leftSideNumChars, "") - + this.indexedModeHelpLine + format("%*s", rightSideNumChars, ""); -} - -// Shows help for the indexed mode list -function DigDistMsgReader_ShowIndexedListHelp() -{ - console.clear("\x01n"); - DisplayProgramInfo(); - console.attributes = "N"; - console.print(this.colors.tradInterfaceHelpScreenColor); - console.print("The current mode is Indexed Mode, which shows total and new messages for any\r\n"); - console.print("sub-boards in your newscan configuration. You may choose a sub-board from this\r\n"); - console.print("list to read messages in that sub-board.\r\n"); - console.crlf(); - if (console.term_supports(USER_ANSI) && this.msgListUseLightbarListInterface) - { - var formatStr = "\x01n\x01h\x01c%15s" + this.colors.tradInterfaceHelpScreenColor + ": %s\r\n"; - var formatStr2 = "\x01n\x01h\x01c%10s \x01n\x01cor \x01h%s" + this.colors.tradInterfaceHelpScreenColor + ": %s\r\n"; - console.crlf(); - displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h"); - console.print(this.colors.tradInterfaceHelpScreenColor); - printf(formatStr, "Down arrow", "Move the cursor down/select the next sub-board"); - printf(formatStr, "Up arrow", "Move the cursor up/select the previous sub-board"); - printf(formatStr2, "PageDown", "N", "Go to the next page"); - printf(formatStr2, "PageUp", "P", "Go to the previous page"); - printf(formatStr2, "HOME", "F", "Go to the first item"); - printf(formatStr2, "END", "L", "Go to the last item"); - printf(formatStr, "ENTER", "Read the sub-board"); - printf(formatStr, "R", "Mark all read"); - printf(formatStr, "M", "Show message list for the sub-board"); - printf(formatStr, "Ctrl-U", "User settings"); - printf(formatStr, "Q", "Quit"); - //printf(formatStr, "?", "Show this help screen"); - } - console.pause(); - console.aborted = false; -} - -// Returns an object with the widest text length of the number of new messsages and -// number of new-to-you messages in all readable sub-boards in the user's newscan configuration -// -// Parameters: -// pScanScope: Numeric - Whether to scan the current sub-board, group, or all. -// This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL. -// pForNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration. -// Defaults to false. -// -// Return value: An object with the following properties: -// widestNumMsgs: The biggest length of the number of messages in the sub-boards -// widestNumNewMsgs: The biggest length of the number of new (unread) messages in the sub-boards -function findWidestNumMsgsAndNumNewMsgs(pScanScope, pForNewscanOnly) -{ - var retObj = { - widestNumMsgs: 0, - widestNumNewMsgs: 0 - }; - - var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL); - var onlyNewscanCfg = (typeof(pForNewscanOnly) === "boolean" ? pForNewscanOnly : false); - - for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx) - { - // If scanning the user's current group or sub-board and this is the wrong group, then skip this group. - if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx) - continue; - - for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx) - { - if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read) - continue; - if (onlyNewscanCfg && !Boolean(msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW)) - continue; - // If scanning the user's current sub-board and this is the wrong sub-board, then - // skip this sub-board (the other groups should have been skipped in the outer loop). - if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx) - continue; - - ++retObj.numSubBoards; - var totalNumMsgsInSub = msg_area.grp_list[grpIdx].sub_list[subIdx].posts; - var totalNumMsgsInSubLen = totalNumMsgsInSub.toString().length; - if (totalNumMsgsInSubLen > retObj.widestNumMsgs) - retObj.widestNumMsgs = totalNumMsgsInSubLen; - var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(msg_area.grp_list[grpIdx].sub_list[subIdx].code); - var numNewMessagesInSubLen = latestPostInfo.numNewMsgs.toString().length; - if (numNewMessagesInSubLen > retObj.widestNumNewMsgs) - retObj.widestNumNewMsgs = numNewMessagesInSubLen; - } - } - return retObj; -} - -// Gets the timestamp of the latest post in a sub-board and number of new messages (unread -// to the user) in a sub-board (based on the user's scan_ptr). -// -// Parameters: -// pSubCode: The internal code of a sub-board to check -// -// Return value: An object with the following properties: -// latestMsgTimestamp: The timestamp of the latest post in the sub-board -// numnewMsgs: The number of new messages (unread to the user) in the sub-board -function getLatestPostTimestampAndNumNewMsgs(pSubCode) -{ - var retObj = { - latestMsgTimestamp: 0, - numNewMsgs: 0 - }; - - var msgbase = new MsgBase(pSubCode); - if (msgbase.open()) - { - retObj.latestMsgTimestamp = getLatestPostTimeWithMsgbase(msgbase, pSubCode); - var totalNumMsgs = msgbase.total_msgs; - // scan_ptr: user's current new message scan pointer (highest-read message number) - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number") - { - var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode); - if (lastReadableMsgHdr != null) - { - // If the user's scan_ptr for the sub-board isn't the 'last message' - // special value, then use it - if (!subBoardScanPtrIsLatestMsgSpecialVal(pSubCode)) - { - // Count the number of readable messages after scan_ptr up to the last read message. - // If both index objects are null, then calculate this via the last readable message - // number and the scan pointer. - var scanPtrMsgIndex = msgbase.get_msg_index(false, msg_area.sub[pSubCode].scan_ptr, false); - //var lastMsgIndex = msgbase.get_msg_index(false, msgbase.last_msg, false); - //if (scanPtrMsgIndex != null && lastMsgIndex != null) - if (scanPtrMsgIndex != null) - { - //for (var i = scanPtrMsgIndex.offset; i < lastMsgIndex.offset; ++i) - for (var i = scanPtrMsgIndex.offset; i < lastReadableMsgHdr.offset; ++i) - { - var msgIndex = msgbase.get_msg_index(true, i, false); - if (msgIndex != null && isReadableMsgHdr(msgIndex, pSubCode)) - ++retObj.numNewMsgs; - } - } - else - { - retObj.numNewMsgs = lastReadableMsgHdr.number - msg_area.sub[pSubCode].scan_ptr; - // Calculating the number of new messages in the above way seems to - // sometimes (though rarely) be incorrect (returning more than the actual - // number of new messages). Another way might be to start from scan_ptr - // scan_ptr and count the number of readable messages. - //retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].scan_ptr); - } - } - } - } - else if (typeof(msg_area.sub[pSubCode].last_read) === "number") - { - var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode); - if (lastReadableMsgHdr != null) - { - retObj.numNewMsgs = lastReadableMsgHdr.number - msg_area.sub[pSubCode].last_read; - // Calculating the number of new messages in the above way seems to - // sometimes (though rarely) be incorrect (returning more than the actual - // number of new messages). Another way might be to start from scan_ptr - // scan_ptr and count the number of readable messages. - //retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].last_read); - } - else // Count the number of new readable messages. - retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].last_read); - } - else - retObj.numNewMsgs = msg_area.sub[pSubCode].posts; - msgbase.close(); - if (retObj.numNewMsgs < 0) - retObj.numNewMsgs = 0; - } - return retObj; -} - -// Creates the DDLightbarMenu object for indexed reader mode -function DigDistMsgReader_CreateLightbarIndexedModeMenu(pNumMsgsWidth, pNumNewMsgsWidth, pLastPostDateWidth, pDescWidth) -{ - // Start & end indexes for the selectable items - var indexMenuIdxes = { - newStatusStart: 0, - newStatusEnd: 4, - descStart: 4, - descEnd: pDescWidth+1, - totalStart: pDescWidth+1, - totalEnd: pDescWidth+pNumMsgsWidth+2, - newMsgsStart: pDescWidth+1+pNumMsgsWidth+1, - newMsgsEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2, - lastPostDateStart: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2, - lastPostDateEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+pLastPostDateWidth+3 - }; - - //var menuHeight = 12; - // For the menu height, -2 gives one row at the top for the column headers and one row - // at the bottom for the help line - var menuHeight = console.screen_rows - 2; - - var indexedModeMenu = new DDLightbarMenu(1, 2, console.screen_columns, menuHeight); - indexedModeMenu.allowUnselectableItems = true; - indexedModeMenu.scrollbarEnabled = true; - indexedModeMenu.borderEnabled = false; - // Colors: - var newStatusHigh = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuNewIndicatorHighlight; - var descHigh = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuDescHighlight; - var totalMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuTotalMsgsHighlight; - var numNewMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuNumNewMsgsHighlight; - var lastPostDateHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuLastPostDateHighlight; - indexedModeMenu.SetColors({ - itemColor: [{start: indexMenuIdxes.newStatusStart, end: indexMenuIdxes.newStatusEnd, attrs: "\x01n" + this.colors.indexMenuNewIndicator}, - {start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: "\x01n" + this.colors.indexMenuDesc}, - {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: "\x01n" + this.colors.indexMenuTotalMsgs}, - {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: "\x01n" + this.colors.indexMenuNumNewMsgs}, - {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: "\x01n" + this.colors.indexMenuLastPostDate}], - selectedItemColor: [{start: indexMenuIdxes.newStatusStart, end: indexMenuIdxes.newStatusEnd, attrs: newStatusHigh}, - {start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: descHigh}, - {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: totalMsgsHi}, - {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: numNewMsgsHi}, - {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: lastPostDateHi}], - unselectableItemColor: "" - }); - - indexedModeMenu.multiSelect = false; - indexedModeMenu.ampersandHotkeysInItems = false; - indexedModeMenu.wrapNavigation = false; - indexedModeMenu.allowANSI = this.msgListUseLightbarListInterface && console.term_supports(USER_ANSI); - - // Add additional keypresses for quitting the menu's input loop so we can - // respond to these keys - // TODO: Include Mm to allow the user to view the message list instead of read it from the indexed menu - //indexedModeMenu.AddAdditionalQuitKeys(); - for (var key in this.indexedModeMenuKeys) - { - if (/[a-zA-Z]/.test(this.indexedModeMenuKeys[key])) - { - indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key].toLowerCase()); - indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key].toUpperCase()); - } - else - indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key]); - } - - // Add additional keypresses for PageUp, PageDown, HOME (first page), and END (last page) - indexedModeMenu.AddAdditionalPageUpKeys("Pp"); // Previous page - indexedModeMenu.AddAdditionalPageDownKeys("Nn"); // Next page - indexedModeMenu.AddAdditionalFirstPageKeys("Ff"); // First page - indexedModeMenu.AddAdditionalLastPageKeys("Ll"); // Last page - - return indexedModeMenu; -} - -// 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 -// pPromptPos: Optional - An object containing x & y coordinates for the prompot position, -// if using the ANSI interfce. If this is a valid object with x & y, this -// will be used for cursor positioning before prompting to save all message -// headers (if applicable). -// -// 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, pPromptPos) -{ - // Sanity checking - if (typeof(pMsgHdr) !== "object") - return({ succeeded: false, errorMsg: "Header object not given"}); - if (typeof(pFilename) != "string") - return({ succeeded: false, errorMsg: "Filename parameter not a string"}); - if (pFilename.length == 0) - return({ succeeded: false, errorMsg: "Empty filename given"}); - - var retObj = { - succeeded: true, - errorMsg: "" - }; - - // Get the message text and save it - // Note: GetMsgInfoForEnhancedReader() can expand @-codes in the message, - // but for now we're saving the message basically as-is. - //var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false); - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - var msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true); - var hdrLines = this.GetExtdMsgHdrInfo(msgbase, pMsgHdr.number, false, false, false, false); - msgbase.close(); - - var messageSaveFile = new File(pFilename); - if (messageSaveFile.open("w")) - { - var writeAllHeaders = false; - if (typeof(this.saveAllHdrsWhenSavingMsgToBBSPC) === "boolean") - writeAllHeaders = this.saveAllHdrsWhenSavingMsgToBBSPC; - else if (typeof(this.saveAllHdrsWhenSavingMsgToBBSPC) === "string" && this.saveAllHdrsWhenSavingMsgToBBSPC.toUpperCase() == "ASK") - { - console.attributes = "N"; - if (typeof(pPromptPos) === "object" && pPromptPos.hasOwnProperty("x") && pPromptPos.hasOwnProperty("y")) - { - console.gotoxy(pPromptPos); - console.cleartoeol("\x01n"); - console.gotoxy(pPromptPos); - } - writeAllHeaders = !console.noyes("Write all headers to saved message"); - } - - if (writeAllHeaders) - { - // Write all header information to the file - for (var i = 0; i < hdrLines.length; ++i) - messageSaveFile.writeln(hdrLines[i]); - } - else - { - // Write to, from, subjetc, etc. to the file - if (this.subBoardCode == "mail") - { - if (!msgIsToCurrentUserByName(pMsgHdr)) - { - messageSaveFile.writeln("From " + pMsgHdr.to + "'s personal email"); - messageSaveFile.writeln("======================="); - } - } - else - { - var line = format("From sub-board: %s, %s", - msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].description, - msg_area.sub[this.subBoardCode].description); - messageSaveFile.writeln(line); - } - messageSaveFile.writeln("From: " + pMsgHdr.from); - messageSaveFile.writeln("To: " + pMsgHdr.to); - messageSaveFile.writeln("Subject: " + pMsgHdr.subject); - // Message time - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr); - var dateTimeStr = ""; - if (msgWrittenLocalTime != -1) - dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime); - else - dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, ""); - messageSaveFile.writeln("Date: " + dateTimeStr); - - } - messageSaveFile.writeln("==============================="); - - // If the message body has ANSI, then use the Graphic object to strip it - // of any cursor movement codes - var msgHasANSICodes = msgBody.indexOf("\x1b[") >= 0; - if (msgHasANSICodes) - { - if (textHasDrawingChars(msgBody)) - { - //var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight); - // To help ensure ANSI messages look good, it seems the Graphic object should have - // its with later set to 1 less than the width used to create it. - var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns); - var graphic = new Graphic(graphicWidth, this.msgAreaHeight); - graphic.auto_extend = true; - graphic.ANSI = ansiterm.expand_ctrl_a(msgBody); - graphic.width = graphicWidth - 1; - msgBody = syncAttrCodesToANSI(graphic.MSG); - } - else - msgBody = syncAttrCodesToANSI(msgBody); - } - - // Write the message body to the file - messageSaveFile.write(msgBody); - messageSaveFile.close(); - } - else - { - retObj.succeeded = false; - retObj.errorMsg = "Failed to open the file for writing"; - } - } - else - { - retObj.succeeded = false; - retObj.errorMsg = "Unable to open the messagebase"; - } - - 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] = {}; - - // 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 (user.is_sysop) - 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, or not deleted. -// -// Parameters: -// pDelete: Boolean - Whether or not the message should be marked deleted. Defaults to true. -// If false, the message will be marked not deleted (if it is marked as deleted). -// -// Return value: An object with the following -// properties: -// opSuccessful: 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_DeleteOrUndeleteSelectedMessages(pDelete) -{ - var retObj = { - opSuccessful: false, - failureList: {} - }; - - var markAsDeleted = (typeof(pDelete) === "boolean" ? pDelete : true); - - var msgBase = null; - var msgHdr = null; - for (var subBoardCode in this.selectedMessages) - { - msgBase = new MsgBase(subBoardCode); - if (msgBase.open()) - { - // If deleting messages, then check whether the user is the sysop, they're - // reading their personal mail, or the sub-board allows deleting messages. - var canContinue = true; - if (markAsDeleted) - canContinue = (user.is_sysop || (subBoardCode == "mail") || ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL)); - if (canContinue) - { - 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 (this.hdrsForCurrentSubBoard.length > 0) - { - if ((msgIdxNumber >= 0) && (msgIdxNumber < this.hdrsForCurrentSubBoard.length)) - msgHdr = this.hdrsForCurrentSubBoard[msgIdxNumber]; - } - else - { - if ((msgIdxNumber >= 0) && (msgIdxNumber < msgBase.total_msgs)) - msgHdr = msgBase.get_msg_header(true, msgIdxNumber, false); - } - // 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) - { - if (markAsDeleted) - { - // remove_msg() just marks a message for deletion - //retObj.opSuccessful = msgBase.remove_msg(true, msgHdr.offset); - retObj.opSuccessful = msgBase.remove_msg(false, msgHdr.number); - } - else - { - // If the message is marked deleted, unmark it. - if ((msgHdr.attr & MSG_DELETE) == MSG_DELETE) - { - var tmpMsgHdr = msgBase.get_msg_header(false, msgHdr.number, false); - if (tmpMsgHdr != null) - { - tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE; - retObj.opSuccessful = msgBase.put_msg_header(false, msgHdr.number, tmpMsgHdr); - } - else - retObj.opSuccessful = false; - } - else - retObj.opSuccessful = true; // No change necessary - } - } - else - retObj.opSuccessful = false; - if (retObj.opSuccessful) - { - // Refresh the message header in the header arrays (if it exists there) and - // remove the message index from the selectedMessages object. Also, delete - // or undelete any vote response messages that may exist for this message. - this.RefreshHdrInSavedArrays(msgIdxNumber, MSG_DELETE, markAsDeleted, subBoardCode, false); - var voteDelRetObj = toggleVoteMsgsDeleted(msgBase, msgHdr.number, msgHdr.id, markAsDeleted, (subBoardCode == "mail")); - if (!voteDelRetObj.allVoteMsgsAffected) - { - retObj.opSuccessful = false; - if (!retObj.failureList.hasOwnProperty(subBoardCode)) - retObj.failureList[subBoardCode] = []; - retObj.failureList[subBoardCode].push(msgIdxNumber); - } - // If deleting and the user can't view deleted messages, then remove the message from this.selectedMessages - if (markAsDeleted && !canViewDeletedMsgs()) - delete this.selectedMessages[subBoardCode][msgIdx]; - } - else - { - retObj.opSuccessful = false; - if (!retObj.failureList.hasOwnProperty(subBoardCode)) - retObj.failureList[subBoardCode] = []; - retObj.failureList[subBoardCode].push(msgIdxNumber); - } - } - if (markAsDeleted) - { - // If the sub-board index array no longer has any properties (i.e., - // all messages in the sub-board were marked as deleted) and the user - // can't view deleted messages, then remove the sub-board property from - // this.selectedMessages - if (Object.keys(this.selectedMessages[subBoardCode]).length == 0 && !canViewDeletedMsgs()) - delete this.selectedMessages[subBoardCode]; - } - } - else - { - if (markAsDeleted && !canContinue) - { - // 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.opSuccessful = false; - retObj.failureList[subBoardCode] = []; - } - } - - 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.opSuccessful = false; - retObj.failureList[subBoardCode] = []; - } - } - - 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; -} - -// Allows the user to forward a message to an email address or -// another user. This function is interactive with the user. -// -// Parameters: -// pMsgHeader: The header of the message being forwarded -// pMsgBody: The body text of the message -// -// Return value: A blank string on success or a string containing a -// message upon failure. -function DigDistMsgReader_ForwardMessage(pMsgHdr, pMsgBody) -{ - if (typeof(pMsgHdr) != "object") - return "Invalid message header given"; - - var retStr = ""; - - console.attributes = "N"; - console.crlf(); - console.print("\x01cUser name/number/email address\x01h:\x01n"); - console.crlf(); - var msgDest = console.getstr(console.screen_columns - 1, K_LINE); - console.attributes = "N"; - console.crlf(); - if (msgDest.length > 0) - { - // Let the user change the subject if they want (prepend it with "Fwd: " for forwarded - // and let the user edit the subject - var subjPromptText = bbs.text(SubjectPrompt); - console.putmsg(subjPromptText); - var initialMsgSubject = (this.prependFowardMsgSubject ? "Fwd: " + pMsgHdr.subject : pMsgHdr.subject); - var msgSubject = console.getstr(initialMsgSubject, console.screen_columns - console.strlen(subjPromptText) - 1, K_LINE | K_EDIT); - - var tmpMsgbase = new MsgBase("mail"); - if (tmpMsgbase.open()) - { - // If the given message body is not a string, then get the - // message body from the messagebase. - if (typeof(pMsgBody) != "string") - { - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - pMsgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true); - msgbase.close(); - } - else - return "Unable to open the sub-board to get the message body"; - } - - // Prepend some lines to the message body to describe where - // the message came from originally. - var newMsgBody = "This is a forwarded message from " + system.name + "\n"; - newMsgBody += "Forwarded by: " + user.alias; - if (user.alias != user.name) - newMsgBody += " (" + user.name + ")"; - newMsgBody += "\n"; - if (this.subBoardCode == "mail") - newMsgBody += "From " + user.name + "'s personal email\n"; - else - { - newMsgBody += "From sub-board: " - + msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].description - + ", " + msg_area.sub[this.subBoardCode].description + "\n"; - } - newMsgBody += "From: " + pMsgHdr.from + "\n"; - newMsgBody += "To: " + pMsgHdr.to + "\n"; - if (msgSubject == pMsgHdr.subject) - newMsgBody += "Subject: " + pMsgHdr.subject + "\n"; - else - { - newMsgBody += "Subject: " + msgSubject + "\n"; - newMsgBody += "(Original subject: " + pMsgHdr.subject + ")\n"; - } - newMsgBody += "==================================\n\n"; - newMsgBody += pMsgBody; - - // Ask whether to edit the message before forwarding it, - // and use console.editfile(filename) to edit it. - if (!console.noyes("Edit the message before sending")) - { - var baseWorkDir = system.node_dir + "DDMsgReader_Temp"; - deltree(baseWorkDir + "/"); - if (mkdir(baseWorkDir)) - { - // TODO: Let the user edit the message, then read it - // and set newMsgBody to it - var tmpMsgFilename = baseWorkDir + "/message.txt"; - // Write the current message to the file - var wroteMsgToTmpFile = false; - var outFile = new File(tmpMsgFilename); - if (outFile.open("w")) - { - wroteMsgToTmpFile = outFile.write(newMsgBody, newMsgBody.length); - outFile.close(); - } - if (wroteMsgToTmpFile) - { - // Let the user edit the file, and if successful, - // read it in to newMsgBody - if (console.editfile(tmpMsgFilename)) - { - var inFile = new File(tmpMsgFilename); - if (inFile.open("r")) - { - newMsgBody = inFile.read(inFile.length); - inFile.close(); - } - } - } - else - { - console.print("\x01n\x01cFailed to write message to a file for editing\x01n"); - console.crlf(); - console.pause(); - } - } - else - { - console.print("\x01n\x01cCouldn't create temporary directory\x01n"); - console.crlf(); - console.pause(); - } - } - // End New (editing message) - - // Create part of a header object which will be used when saving/sending - // the message. The destination ("to" informatoin) will be filled in - // according to the destination type. - var destMsgHdr = { to_net_type: NET_NONE, from: user.name, - replyto: user.name, subject: msgSubject }; // pMsgHdr.subject - if (user.netmail.length > 0) - { - destMsgHdr.replyto_net_addr = user.netmail; - } - else - { - destMsgHdr.replyto_net_addr = user.email; - } - //destMsgHdr.when_written_time = - //destMsgHdr.when_written_zone = system.timezone; - //destMsgHdr.when_written_zone_offset = - - var confirmedForwardMsg = true; - - // If the destination is in the format anything@anything, then - // accept it as the message destination. It could be an Internet - // address (someone@somewhere.com), FidoNet address (sysop@1:327/4), - // or a QWK address (someone@HOST). - // We could specifically use gEmailRegex and gFTNEmailRegex to test - // msgDest, but just using those would be too restrictive. - if (/^.*@.*$/.test(msgDest)) - { - confirmedForwardMsg = console.yesno("Forward via email to " + msgDest); - if (confirmedForwardMsg) - { - console.print("\x01n\x01cForwarding via email to " + msgDest + "\x01n"); - console.crlf(); - destMsgHdr.to = msgDest; - destMsgHdr.to_net_addr = msgDest; - destMsgHdr.to_net_type = netaddr_type(msgDest); - } - } - else - { - // See if what the user entered is a user number/name/alias - var userNum = 0; - if (/^[0-9]+$/.test(msgDest)) - { - userNum = +msgDest; - // Determine if the user entered a valid user number - var lastUserNum = (system.lastuser == undefined ? system.stats.total_users : system.lastuser + 1); - if ((userNum < 1) || (userNum >= lastUserNum)) - { - userNum = 0; - console.print("\x01h\x01y* Invalid user number (" + msgDest + ")\x01n"); - console.crlf(); - } - } - else // Try to get a user number assuming msgDest is a username/alias - userNum = system.matchuser(msgDest, true); - // If we have a valid user number, then we can forward the message. - if (userNum > 0) - { - var destUser = new User(userNum); - confirmedForwardMsg = console.yesno("Forward to " + destUser.alias + " (user " + destUser.number + ")"); - if (confirmedForwardMsg) - { - destMsgHdr.to = destUser.alias; - // If the destination user has an Internet email address, - // ask the user if they want to send to the destination - // user's Internet email address - var sendToNetEmail = false; - if (destUser.netmail.length > 0) - { - sendToNetEmail = !console.noyes("Send to the user's Internet email (" + destUser.netmail + ")"); - if (sendToNetEmail) - { - console.print("\x01n\x01cForwarding to " + destUser.netmail + "\x01n"); - console.crlf(); - destMsgHdr.to = destUser.name; - destMsgHdr.to_net_addr = destUser.netmail; - destMsgHdr.to_net_type = NET_INTERNET; - } - } - if (!sendToNetEmail) - { - console.print("\x01n\x01cForwarding to " + destUser.alias + "\x01n"); - console.crlf(); - destMsgHdr.to_ext = destUser.number; - destMsgHdr.to_net_type = NET_NONE; - } - } - } - else - { - confirmedForwardMsg = false; - console.print("\x01h\x01y* Unknown destination\x01n"); - console.crlf(); - } - } - var savedMsg = true; - if (confirmedForwardMsg) - savedMsg = tmpMsgbase.save_msg(destMsgHdr, newMsgBody); - else - { - console.print("\x01n\x01cCanceled\x01n"); - console.crlf(); - } - tmpMsgbase.close(); - - if (!savedMsg) - { - console.print("\x01h\x01y* Failed to send the message!\x01n"); - console.crlf(); - } - - // Pause for user input so the user can see the messages written - console.pause(); - } - else - retStr = "Failed to open email messagebase"; - } - else - { - console.print("\x01n\x01cCanceled\x01n"); - console.crlf(); - console.pause(); - } - - return retStr; -} - -function printMsgHdrInfo(pMsgHdr) -{ - if (typeof(pMsgHdr) != "object") - return; - - for (var prop in pMsgHdr) - { - if (prop == "to_net_type") - print(prop + ": " + toNetTypeToStr(pMsgHdr[prop])); - else - console.print(prop + ": " + pMsgHdr[prop]); - console.crlf(); - } -} - -function toNetTypeToStr(toNetType) -{ - var toNetTypeStr = "Unknown"; - if (typeof(toNetType) == "number") - { - switch (toNetType) - { - case NET_NONE: - toNetTypeStr = "Local"; - break; - case NET_UNKNOWN: - toNetTypeStr = "Unknown networked"; - break; - case NET_FIDO: - toNetTypeStr = "FidoNet"; - break; - case NET_POSTLINK: - toNetTypeStr = "PostLink"; - break; - case NET_QWK: - toNetTypeStr = "QWK"; - break; - case NET_INTERNET: - toNetTypeStr = "Internet"; - break; - default: - toNetTypeStr = "Unknown"; - break; - } - } - return toNetTypeStr; -} - -// For the DigDistMsgReader class: Lets the user vote on a message -// -// Parameters: -// pMsgHdr: The header of the mesasge being voted on -// pRemoveNLsFromVoteText: Optional boolean - Whether or not to remove newlines -// (and carriage returns) from the voting text from -// text.dat. Defaults to false. -// -// Return value: An object with the following properties: -// BBSHasVoteFunction: Boolean - Whether or not the system has -// the vote_msg function -// savedVote: Boolean - Whether or not the vote was saved -// userQuit: Boolean - Whether or not the user quit and didn't vote -// errorMsg: String - An error message, if something went wrong -// mnemonicsRequiredForErrorMsg: Boolean - Whether or not mnemonics is required to print the error message -// updatedHdr: The updated message header containing vote information. -// If something went wrong, this will be null. -function DigDistMsgReader_VoteOnMessage(pMsgHdr, pRemoveNLsFromVoteText) -{ - var retObj = { - BBSHasVoteFunction: false, - savedVote: false, - userQuit: false, - errorMsg: "", - mnemonicsRequiredForErrorMsg: false, - updatedHdr: null - }; - - // Don't allow voting for personal email - if (this.subBoardCode == "mail") - { - retObj.errorMsg = "Can not vote on personal email"; - return retObj; - } - - // Check whether the user has the voting restiction - if ((user.security.restrictions & UFLAG_V) == UFLAG_V) - { - // Use the line from text.dat that says the user is not allowed to vote, - // and remove newlines from it. - retObj.errorMsg = "\x01n" + bbs.text(typeof(R_Voting) != "undefined" ? R_Voting : 781).replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", ""); - return retObj; - } - - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - { - return retObj; - } - - // If the message vote function is not defined in the running verison of Synchronet, - // then just return. - retObj.BBSHasVoteFunction = (typeof(msgbase.vote_msg) == "function"); - if (!retObj.BBSHasVoteFunction) - { - msgbase.close(); - return retObj; - } - - var removeNLsFromVoteText = (typeof(pRemoveNLsFromVoteText) === "boolean" ? pRemoveNLsFromVoteText : false) - - // See if voting is allowed in the current sub-board - if ((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == SUB_NOVOTING) - { - retObj.errorMsg = bbs.text(typeof(VotingNotAllowed) != "undefined" ? VotingNotAllowed : 779); - if (removeNLsFromVoteText) - retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", ""); - retObj.mnemonicsRequiredForErrorMsg = true; - msgbase.close(); - return retObj; - } - - // If the message is a poll question and has the maximum number of votes - // already or is closed for voting, then don't let the user vote on it. - if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL) - { - var userVotedMaxVotes = false; - var numVotes = (pMsgHdr.hasOwnProperty("votes") ? pMsgHdr.votes : 0); - if (typeof(msgbase.how_user_voted) === "function") - { - var votes = msgbase.how_user_voted(pMsgHdr.number, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias); - // TODO: I'm not sure if this 'if' section is correct anymore for - // the latest 3.17 build of Synchronet (August 14, 2017) - // Digital Man said: - // In a poll message, the "votes" property specifies the maximum number of - // answers/votes per ballot (0 is the equivalent of 1). - // Max votes testing? : - // userVotedMaxVotes = (votes == pMsgHdr.votes); - if (votes >= 0) - { - if ((votes == 0) || (votes == 1)) - userVotedMaxVotes = (votes == 3); // (1 << 0) | (1 << 1); - else - { - userVotedMaxVotes = true; - for (var voteIdx = 0; voteIdx <= numVotes; ++voteIdx) - { - if (votes && (1 << voteIdx) == 0) - { - userVotedMaxVotes = false; - break; - } - } - } - } - } - var pollIsClosed = ((pMsgHdr.auxattr & POLL_CLOSED) == POLL_CLOSED); - if (pollIsClosed) - { - retObj.errorMsg = "This poll is closed"; - msgbase.close(); - return retObj; - } - else if (userVotedMaxVotes) - { - retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780); - if (removeNLsFromVoteText) - retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", ""); - retObj.mnemonicsRequiredForErrorMsg = true; - msgbase.close(); - return retObj; - } - } - - // If the user has voted on this message already, then set an error message and return. - if (this.HasUserVotedOnMsg(pMsgHdr.number)) - { - retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780); - if (removeNLsFromVoteText) - retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", ""); - retObj.mnemonicsRequiredForErrorMsg = true; - msgbase.close(); - return retObj; - } - - // New MsgBase method: vote_msg(). it takes a message header object - // (like save_msg), except you only need a few properties, in order of - // importarnce: - // attr: you need to have this set to MSG_UPVOTE, MSG_DOWNVOTE, or MSG_VOTE - // thread_back or reply_id: either of these must be set to indicate msg to vote on - // from: name of voter - // from_net_type and from_net_addr: if applicable - - // Do some initial setup of the header for the vote message to be - // saved to the messagebase - var voteMsgHdr = { - thread_back: pMsgHdr.number, - reply_id: pMsgHdr.id, - from: (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias - }; - if (pMsgHdr.from.hasOwnProperty("from_net_type")) - { - voteMsgHdr.from_net_type = pMsgHdr.from_net_type; - if (pMsgHdr.from_net_type != NET_NONE) - voteMsgHdr.from_net_addr = user.email; - } - - // Input vote options from the user differently depending on whether - // the message is a poll or not - if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL) - { - if (pMsgHdr.hasOwnProperty("field_list")) - { - console.clear("\x01n"); - var selectHdr = bbs.text(typeof(BallotHdr) != "undefined" ? BallotHdr : 791); - printf("\x01n" + selectHdr + "\x01n", pMsgHdr.subject); - var optionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01h%s\x01n"; - var optionNum = 1; - for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI) - { - if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER) - { - printf(optionFormatStr, optionNum++, pMsgHdr.field_list[fieldI].data); - console.crlf(); - } - } - console.crlf(); - // Get & process the selection from the user - var voteResponse = 0; - if (pMsgHdr.votes > 1) - { - // Support multiple answers from the user - console.print("\x01n\x01gYour vote numbers, separated by commas, up to \x01h" + pMsgHdr.votes + "\x01n\x01g (Blank/Q=Quit):"); - console.crlf(); - //console.print("\x01c\x01h"); - console.attributes = "CH"; - var userInput = consoleGetStrWithValidKeys("0123456789,Q", null, false); - if ((userInput.length > 0) && (userInput.toUpperCase() != "Q")) - { - var userAnswers = userInput.split(","); - if (userAnswers.length > 0) - { - // Generate confirmation text and an array of numbers - // representing the user's choices, up to the number - // of responses allowed - var confirmText = "Vote "; - var voteNumbers = []; - for (var i = 0; (i < userAnswers.length) && (i < pMsgHdr.votes); ++i) - { - // Trim any whitespace from the user's response - userAnswers[i] = trimSpaces(userAnswers[i], true, true, true); - if (/^[0-9]+$/.test(userAnswers[i])) - { - voteNumbers.push(+userAnswers[i]); - confirmText += userAnswers[i] + ","; - } - } - // If the confirmation text has a trailing comma, remove it - if (/,$/.test(confirmText)) - confirmText = confirmText.substr(0, confirmText.length-1); - // Confirm from the user and submit their vote if they say yes - if (voteNumbers.length > 0) - { - if (console.yesno(confirmText)) - { - voteResponse = 0; - for (var i = 0; i < voteNumbers.length; ++i) - voteResponse |= (1 << (voteNumbers[i]-1)); - } - else - retObj.userQuit = true; - } - } - } - else - retObj.userQuit = true; - } - else - { - // Get the selection prompt text from text.dat and replace the %u or %d with - // the number 1 (default option) - var selectPromptText = bbs.text(SelectItemWhich); - selectPromptText = selectPromptText.replace(/%[uU]/, 1).replace(/%[dD]/, 1); - console.mnemonics(selectPromptText); - var maxNum = optionNum - 1; - var userInputNum = console.getnum(maxNum); - if (userInputNum == -1) // The user chose Q to quit - retObj.userQuit = true; - else - voteResponse = (1 << (userInputNum-1)); - console.attributes = "N"; - } - if (!retObj.userQuit) - { - voteMsgHdr.attr = MSG_VOTE; - voteMsgHdr.votes = voteResponse; - } - } - } - else - { - // The message is not a poll - Prompt for up/downvote - if ((typeof(MSG_UPVOTE) != "undefined") && (typeof(MSG_DOWNVOTE) != "undefined")) - { - var voteAttr = 0; - // Get text line 783 to prompt for voting - var textDatText = bbs.text(typeof(VoteMsgUpDownOrQuit) != "undefined" ? VoteMsgUpDownOrQuit : 783); - if (removeNLsFromVoteText) - textDatText = textDatText.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", ""); - console.attributes = "N"; - console.mnemonics(textDatText); - console.attributes = "N"; - // Using getAllowedKeyWithMode() instead of console.getkeys() so we - // can control the input mode better, so it doesn't output a CRLF - switch (getAllowedKeyWithMode("UDQ" + KEY_UP + KEY_DOWN, K_NOCRLF|K_NOSPIN)) - { - case "U": - case KEY_UP: - voteAttr = MSG_UPVOTE; - break; - case "D": - case KEY_DOWN: - voteAttr = MSG_DOWNVOTE; - break; - case "Q": - default: - retObj.userQuit = true; - break; - } - // If the user voted, then save the user's vote in the attr property - // in the header - if (voteAttr != 0) - voteMsgHdr.attr = voteAttr; - } - else - retObj.errorMsg = "MSG_UPVOTE & MSG_DOWNVOTE are not defined"; - } - - // If the user hasn't quit and there is no error message, then save the vote - // message header - if (!retObj.userQuit && (retObj.errorMsg.length == 0)) - { - console.print("\x01n Submitting.."); - retObj.savedVote = msgbase.vote_msg(voteMsgHdr); - // If the save was successful, then update - // this.hdrsForCurrentSubBoard with the updated - // message header (for the message that was read) - if (retObj.savedVote) - { - if (this.msgNumToIdxMap.hasOwnProperty(pMsgHdr.number)) - { - var originalMsgIdx = this.msgNumToIdxMap[pMsgHdr.number]; - // Calling get_all_msg_headers() to include vote information: - var tmpHdrs = msgbase.get_all_msg_headers(true); - if (tmpHdrs.hasOwnProperty(pMsgHdr.number)) - { - this.hdrsForCurrentSubBoard[originalMsgIdx] = tmpHdrs[pMsgHdr.number]; - // Originally, this script assigned retObj.updatedHdr as follows: - //retObj.updatedHdr = pMsgHdr; - // However, after an update, there were a couple errors that total_votes and upvotes - // were read-only, so it wuldn't assign to them, so now we copy pMsgHdr this way: - retObj.updatedHdr = {}; - for (var prop in pMsgHdr) - retObj.updatedHdr[prop] = pMsgHdr[prop]; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes")) - retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes")) - retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally")) - retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally; - } - // I thought we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - /* - var hdrWithVotes = msgbase.get_msg_header(false, pMsgHdr.number, true, true); - if (hdrWithVotes != null) - { - this.hdrsForCurrentSubBoard[originalMsgIdx] = hdrWithVotes; - // Originally, this script assigned retObj.updatedHdr as follows: - //retObj.updatedHdr = pMsgHdr; - // However, after an update, there were a couple errors that total_votes and upvotes - // were read-only, so it wuldn't assign to them, so now we copy pMsgHdr this way: - retObj.updatedHdr = {}; - for (var prop in pMsgHdr) - retObj.updatedHdr[prop] = pMsgHdr[prop]; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes")) - retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes")) - retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes; - if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally")) - retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally; - } - */ - } - } - else - { - // Failed to save the vote - retObj.errorMsg = "Failed to save your vote"; - } - } - - msgbase.close(); - - return retObj; -} - -// For the DigDistMsgReader class: Checks to see whether a user has voted on a message. -// The message must belong to the currently-open sub-board. -// -// Parameters: -// pMsgNum: The message number -// pUser: Optional - A user account to check. If omitted, the current logged-in -// user will be used. -function DigDistMsgReader_HasUserVotedOnMsg(pMsgNum, pUser) -{ - // Don't do this for personal email - if (this.subBoardCode == "mail") - return false; - - // Thanks to echicken for explaining how to check this. To check a user's - // vote, use MsgBase.how_user_voted(). - /* - The return value will be: - 0 - user hasn't voted - 1 - upvoted - 2 - downvoted - Or, if the message was a poll, it's a bitfield: - if (votes&(1<<2)) { - // User selected answer 2 - } - */ - var userHasVotedOnMsg = false; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - if (typeof(msgbase.how_user_voted) === "function") - { - var votes = 0; - if (typeof(pUser) == "object") - votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? pUser.name : pUser.alias); - else - votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias); - userHasVotedOnMsg = (votes > 0); - } - msgbase.close(); - } - return userHasVotedOnMsg; -} - -// Gets information about the upvotes and downvotes for a message. -// If the user is a sysop, this will also get who voted on the message. -// -// Parameters: -// pMsgHdr: A header of a message that has upvotes & downvotes -// -// Return value: An array of strings containing information about the upvotes, -// downvotes, tally, and (if the user is a sysop) who submitted -// votes on the message. -function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr) -{ - // If the message header doesn't have the "total_votes" or "upvotes" properties, - // then there's no vote information, so just return an empty array. - if (!pMsgHdr.hasOwnProperty("total_votes") || !pMsgHdr.hasOwnProperty("upvotes")) - return []; - - var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHdr); - var voteInfo = []; - voteInfo.push("Upvotes: " + msgVoteInfo.upvotes); - voteInfo.push("Downvotes: " + msgVoteInfo.downvotes); - voteInfo.push("Score: " + msgVoteInfo.voteScore); - if (pMsgHdr.hasOwnProperty("tally")) - voteInfo.push("Tally: " + pMsgHdr.tally); - - // If the user is the sysop, then also add the names of people who - // voted on the message. - if (user.is_sysop) - { - // Check all the messages in the messagebase after the current one - // to find response messages - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - // Pass true to get_all_msg_headers() to tell it to return vote messages - // (the parameter was introduced in Synchronet 3.17+) - var tmpHdrs = msgbase.get_all_msg_headers(true); - for (var tmpProp in tmpHdrs) - { - if (tmpHdrs[tmpProp] == null) - continue; - // If this header's thread_back or reply_id matches the poll message - // number, then append the 'user voted' string to the message body. - if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id)) - { - var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number, false, false, true, true); - if ((tmpHdrs[tmpProp].field_list.length == 0) && (tmpMessageBody.length == 0)) - { - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]); - var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime); - voteInfo.push("\x01n\x01c\x01h" + tmpHdrs[tmpProp].from + "\x01n\x01c voted on this message on " + voteDate + "\x01n"); - } - } - } - msgbase.close(); - } - } - - return voteInfo; -} - -// For the DigDistMsgReader class: Gets the body (text) of a message. If it's -// a poll, this method will format the message body with poll results. Otherwise, -// this method will simply get the message body. -// -// Parameters: -// pMsgHeader: The message header -// -// Return value: An object with the following properties: -// msgBody: The message body -// pmode: The mode flags to be used when printing the message body -function DigDistMsgReader_GetMsgBody(pMsgHdr) -{ - var retObj = { - msgBody: "", - pmode: 0 - }; - - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - return retObj; - - retObj.pmode = msg_pmode(msgbase, pMsgHdr); - - if ((typeof(MSG_TYPE_POLL) != "undefined") && (pMsgHdr.type & MSG_TYPE_POLL) == MSG_TYPE_POLL) - { - // A poll is intended to be parsed (and displayed) using on the header data. The - // (optional) comments are stored in the hdr.field_list[] with type values of - // SMB_COMMENT (now defined in sbbsdefs.js) and the available answers are in the - // field_list[] with type values of SMB_POLL_ANSWER. - - // The 'comments' and 'answers' are also a part of the message header, so you can - // grab them separately, then format and display them however you want. You can - // find them in the header.field_list array; each element in that array should be - // an object with a 'type' and a 'data' property. Relevant types here are - // SMB_COMMENT and SMB_POLL_ANSWER. (This is what I'm doing on the web, and I - // just ignore the message body for poll messages.) - - if (pMsgHdr.hasOwnProperty("field_list")) - { - // Figure out the longest length of the poll choices, with - // a maximum of 22 characters less than the terminal width. - // Use a minimum of 27 characters. - // That length will be used for the formatting strings for - // the poll results. - var voteOptDescLen = 0; - for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI) - { - if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER) - { - if (pMsgHdr.field_list[fieldI].data.length > voteOptDescLen) - voteOptDescLen = pMsgHdr.field_list[fieldI].data.length; - } - } - if (voteOptDescLen > console.screen_columns - 22) - voteOptDescLen = console.screen_columns - 22; - else if (voteOptDescLen < 27) - voteOptDescLen = 27; - - // Format strings for outputting the voting option lines - var unvotedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n"; - var votedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01" + "5\x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n"; - // Add up the total number of votes so that we can - // calculate vote percentages. - var totalNumVotes = 0; - if (pMsgHdr.hasOwnProperty("tally")) - { - for (var tallyI = 0; tallyI < pMsgHdr.tally.length; ++tallyI) - totalNumVotes += pMsgHdr.tally[tallyI]; - } - // Go through field_list and append the voting options and stats to - // retObj.msgBody - var pollComment = ""; - var optionNum = 1; - var numVotes = 0; - var votePercentage = 0; - var tallyIdx = 0; - for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI) - { - if (pMsgHdr.field_list[fieldI].type == SMB_COMMENT) - pollComment += pMsgHdr.field_list[fieldI].data + "\r\n"; - else if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER) - { - // Figure out the number of votes on this option and the - // vote percentage - if (pMsgHdr.hasOwnProperty("tally")) - { - if (tallyIdx < pMsgHdr.tally.length) - { - numVotes = pMsgHdr.tally[tallyIdx]; - votePercentage = (numVotes / totalNumVotes) * 100; - } - } - // Append to the message text - retObj.msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr, - optionNum++, pMsgHdr.field_list[fieldI].data.substr(0, voteOptDescLen), - numVotes, votePercentage); - if (numVotes > 0) - retObj.msgBody += " " + CHECK_CHAR; - retObj.msgBody += "\r\n"; - ++tallyIdx; - } - } - if (pollComment.length > 0) - retObj.msgBody = pollComment + "\r\n" + retObj.msgBody; - - // If voting is allowed in this sub-board and the current logged-in - // user has not voted on this message, then append some text saying - // how to vote. - var votingAllowed = ((this.subBoardCode != "mail") && (((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == 0))); - if (votingAllowed && !this.HasUserVotedOnMsg(pMsgHdr.number)) - retObj.msgBody += "\x01n\r\n\x01gTo vote in this poll, press \x01w\x01h" + this.enhReaderKeys.vote + "\x01n\x01g now.\r\n"; - - // If the current logged-in user created this poll, then show the - // users who have voted on it so far. - var msgFromUpper = pMsgHdr.from.toUpperCase(); - if ((msgFromUpper == user.name.toUpperCase()) || (msgFromUpper == user.handle.toUpperCase())) - { - // Check all the messages in the messagebase after the current one - // to find poll response messages - // Get the line from text.dat for writing who voted & when. It - // is a format string and should look something like this: - //"\r\n\x01n\x01hOn %s, in \x01c%s \x01n\x01c%s\r\n\x01h\x01m%s voted in your poll: \x01n\x01h%s\r\n" 787 PollVoteNotice - var userVotedInYourPollText = bbs.text(typeof(PollVoteNotice) != "undefined" ? PollVoteNotice : 787); - - // Pass true to get_all_msg_headers() to tell it to return vote messages - // (the parameter was introduced in Synchronet 3.17+) - var tmpHdrs = msgbase.get_all_msg_headers(true); - for (var tmpProp in tmpHdrs) - { - if (tmpHdrs[tmpProp] == null) - continue; - // If this header's thread_back or reply_id matches the poll message - // number, then append the 'user voted' string to the message body. - if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id)) - { - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]); - var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime); - var grpName = ""; - var msgbaseCfgName = ""; - var msgbase = new MsgBase(this.subBoardCode); - if (msgbase.open()) - { - grpName = msgbase.cfg.grp_name; - msgbaseCfgName = msgbase.cfg.name; - msgbase.close(); - } - retObj.msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject); - } - } - } - } - } - else - { - // If the message is UTF8 and the terminal is not UTF8-capable, then convert - // the text to cp437. - retObj.msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true); - if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8) - { - var userConsoleSupportsUTF8 = false; - if (typeof(USER_UTF8) != "undefined") - userConsoleSupportsUTF8 = console.term_supports(USER_UTF8); - if (!userConsoleSupportsUTF8) - retObj.msgBody = utf8_cp437(retObj.msgBody); - } - // Remove any initial coloring from the message body, which can color the whole message - retObj.msgBody = removeInitialColorFromMsgBody(retObj.msgBody); - // For HTML-formatted messages, convert HTML entities - if (pMsgHdr.hasOwnProperty("text_subtype") && pMsgHdr.text_subtype.toLowerCase() == "html") - { - retObj.msgBody = html2asc(retObj.msgBody); - // Remove excessive blank lines after HTML-translation - retObj.msgBody = retObj.msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n'); - } - } - msgbase.close(); - - // Remove any Synchronet pause codes that might exist in the message - retObj.msgBody = retObj.msgBody.replace("\x01p", "").replace("\x01P", ""); - - // If the user is a sysop, this is a moderated message area, and the message - // hasn't been validated, then prepend the message with a message to let the - // sysop now know to validate it. - if (this.subBoardCode != "mail") - { - if (user.is_sysop && msg_area.sub[this.subBoardCode].is_moderated && ((pMsgHdr.attr & MSG_VALIDATED) == 0)) - { - var validateNotice = "\x01n\x01h\x01yThis is an unvalidated message in a moderated area. Press " - + this.enhReaderKeys.validateMsg + " to validate it.\r\n\x01g"; - for (var i = 0; i < 79; ++i) - validateNotice += HORIZONTAL_SINGLE; - validateNotice += "\x01n\r\n"; - retObj.msgBody = validateNotice + retObj.msgBody; - } - } - - - // If this message has been marked for deletion, prepend a couple lines saying so - if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE) - { - var deletedNotice = "\x01n\x01h\x01yThis message has been marked for deletion."; - if (user.is_sysop) - deletedNotice += " To un-mark, return to the message list and press U to un-mark this message."; - deletedNotice += "\x01n\r\n\r\n"; - retObj.msgBody = deletedNotice + retObj.msgBody; - } - - return retObj; -} - -// For the DigDistMsgReader class: Refreshes a message header in one of the -// internal message arrays. -// -// Parameters: -// pMsgNum: The number of the message to replace -function DigDistMsgReader_RefreshMsgHdrInArrays(pMsgNum) -{ - var msgbase = new MsgBase(this.subBoardCode); - if (!msgbase.open()) - return; - if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && - (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0)) - { - for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i) - { - if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == pMsgNum) - { - var newMsgHdr = msgbase.get_msg_header(false, pMsgNum, true, true); - if (newMsgHdr != null) - this.msgSearchHdrs[this.subBoardCode].indexed[i] = newMsgHdr; - break; - } - } - } - else if (this.hdrsForCurrentSubBoard.length > 0) - { - if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum)) - { - // Calling get_all_msg_headers() to include vote information: - var msgHdrs = msgbase.get_all_msg_headers(true); - if (msgHdrs.hasOwnProperty(pMsgNum)) - { - var msgIdx = this.msgNumToIdxMap[pMsgNum]; - this.hdrsForCurrentSubBoard[msgIdx] = msgHdrs[pMsgNum]; - } - // I thought we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - /* - var updatedMsgHdr = msgbase.get_msg_header(false, pMsgNum, true, true); - if (updatedMsgHdr != null) - { - var msgIdx = this.msgNumToIdxMap[pMsgNum]; - this.hdrsForCurrentSubBoard[msgIdx] = updatedMsgHdr; - } - */ - } - } - msgbase.close(); -} - -// For the DigDistMessageReader class: Re-calculates the message list widths and -// format strings -// -// Parameters: -// pMsgNumLen: Optional - Length to use for the message number field. If not specified, -// then this will get the number of messages in the sub-board and use that -// length. -function DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs(pMsgNumLen) -{ - // Note: Constructing these strings must be done after reading the configuration - // file in order for the configured colors to be used - - // TODO: Having the separate printf strings for regular, to-user, and from-user - // are a bit pointless now that coloring & alternate coloring is done via - // DDLightbarMenu - this.sMsgListHdrFormatStr = ""; - this.sMsgInfoFormatStr = ""; - this.sMsgInfoToUserFormatStr = ""; - this.sMsgInfoFromUserFormatStr = ""; - this.sMsgInfoFormatHighlightStr = ""; - - this.MSGNUM_LEN = (typeof(pMsgNumLen) == "number" ? pMsgNumLen : this.NumMessages().toString().length); - if (this.MSGNUM_LEN < 4) - 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 - this.FROM_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0); - this.TO_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0); - //var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces - //this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0); - this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-8; // 8 to account for the spaces - - if (this.showScoresInMsgList) - { - this.SUBJ_LEN -= (this.SCORE_LEN + 1); - this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-" - + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %" - + this.SCORE_LEN + "s %-" + this.DATE_LEN + "s %-" - + this.TIME_LEN + "s"; - - 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.msgListScoreColor + "%" + this.SCORE_LEN + "d " - + 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.msgListToUserScoreColor + "%" - + this.SCORE_LEN + "d " + 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.msgListFromUserScoreColor + "%" - + this.SCORE_LEN + "d " + 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.msgListScoreHighlightColor + "%" + this.SCORE_LEN + "d " - + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s " - + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s"; - } - else - { - this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-" - + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-" - + this.DATE_LEN + "s %-" + this.TIME_LEN + "s"; - - 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 header format string (we won't be able to move the cursor). - if (!canDoHighASCIIAndANSI()) - this.sMsgListHdrFormatStr += "\r\n"; -} - -// For the DigDistMessageReader class: Writes a temporary error message at the key help line -// for lightbar mode. -// -// Parameters: -// pErrorMsg: The error message to write -// pHelpLineRefreshDef: Optional - Specifies which help line to refresh on the screen -// (i.e., REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE) -function DigDistMsgReader_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pLineRefreshDef) -{ - console.gotoxy(1, console.screen_rows); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_rows); - console.print("\x01y\x01h" + pErrorMsg + "\x01n"); - mswait(ERROR_WAIT_MS); - var helpLineRefreshDef = (typeof(pHelpLineRefreshDef) == "number" ? pHelpLineRefreshDef : -1); - if (helpLineRefreshDef == REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE) - this.WriteChgMsgAreaKeysHelpLine(); -} - -// For the scrollable reader interface: Refreshes a rectangular region on the screen -// by printing part of the message text. -// -// pTxtLines: The array of text lines of the message being displayed -// pTopLineIdx: The index of the text line currently at the top row in the reader area -// pTopLeftX: The upper-left corner column of the rectangle to be refreshed (1-based) - Absolute screen coordinate -// pTopLeftY: The upper-left corner row of the rectangle to be refreshed (1-based) - Absolute screen coordinate -// pWidth: The width of the rectangle to be refreshed -// pHeight: The height of the rectangle to be refreshed -function DigDistMsgReader_RefreshMsgAreaRectangle(pTxtLines, pTopLineIdx, pTopLeftX, pTopLeftY, pWidth, pHeight) -{ - if (typeof(pTxtLines) !== "object") - return; - if (typeof(pTopLineIdx) !== "number" || pTopLineIdx < 0 || pTopLineIdx >= pTxtLines.length) - return; - if (typeof(pTopLeftX) !== "number" || pTopLeftX < 1 || pTopLeftX > console.screen_columns) - return; - if (typeof(pTopLeftY) !== "number" || pTopLeftY < 1 || pTopLeftY > console.screen_rows) - return; - if (typeof(pWidth) !== "number" || pWidth <= 0 || typeof(pHeight) !== "number" || pHeight <= 0) - return; - - var firstTxtLineIdx = pTopLeftY - pTopLineIdx - 1; - if (firstTxtLineIdx < 0) firstTxtLineIdx = 0; // Shouldn't happen, but just in case - //this.msgAreaLeft = 1; - //this.msgAreaRight = console.screen_columns - 1; - //this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1; - //this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1; - // Sanity checking - if (pTopLeftY < this.msgAreaTop) - { - var diff = this.msgAreaTop - pTopLeftY; - pTopLeftY = this.msgAreaTop; - pTopLineIdx += diff; - } - var lastScreenRow = pTopLeftY + pHeight - 1; // Inclusive, for loop - if (lastScreenRow > this.msgAreaBottom) - lastScreenRow = this.msgAreaBottom; - if (pTopLeftX + pWidth > this.msgAreaRight) - pWidth = this.msgAreaRight - pTopLeftX + 1; - - // Print the parts of the text lines that make up the rectangle - var txtLineIdx = pTopLineIdx + (pTopLeftY-this.msgAreaTop); - if (txtLineIdx < 0) txtLineIdx = 0; // Just in case, but shouldn't happen - var txtLineStartIdx = pTopLeftX - this.msgAreaLeft; // Within each text line (it seemed right to subtract 1 but it wasn't) - if (txtLineStartIdx < 0) txtLineStartIdx = 0; // Just in case, but shouldn't happen - var emptyFormatStr = "\x01n%" + pWidth + "s"; // For printing empty strings after printing all text lines - console.attributes = "N"; - for (var screenRow = pTopLeftY; screenRow <= lastScreenRow; ++screenRow) - { - console.gotoxy(pTopLeftX, screenRow); - // If the current text line index is within the array of text lines, then output the section of the - // text line. Otherwise, output an empty string. - if (txtLineIdx < pTxtLines.length) - { - if (txtLineStartIdx < console.strlen(pTxtLines[txtLineIdx])) - { - // Get the text attributes up to the current point and output them - //console.print(getAllEditLineAttrsUntilLineIdx(pTxtLines, txtLineIdx, true, txtLineStartIdx)); - // Get the section of line (and make sure it can fill the needed width), and print it - // Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js - var lineText = "\x01n" + substrWithAttrCodes(pTxtLines[txtLineIdx].replace(/[\r\n]+/g, ""), txtLineStartIdx, pWidth); - var printableTxtLen = console.strlen(lineText); - if (printableTxtLen < pWidth) - lineText += format("\x01n%*s", pWidth - printableTxtLen, ""); - console.print(lineText); - } - else // The start index is beyond the length of the string, so print an empty string - printf(emptyFormatStr, ""); - } - else // We've printed all the remaining text lines, so now print an empty string. - printf(emptyFormatStr, ""); - - ++txtLineIdx; - } -} - -// For the scrollable reader interface: Returns whether a 'from' or 'to' name in a message header -// is in the user's personal twitlist. -// -// Parameters: -// pMsgHdr: A message header to check -// -// Return value: Boolean - Whether or not the header 'from' or 'to' name is in the user's personal twitlist -function DigDistMsgReader_MsgHdrFromOrToInUserTwitlist(pMsgHdr) -{ - if (pMsgHdr == null || typeof(pMsgHdr) !== "object") - return false; - if (!pMsgHdr.hasOwnProperty("from") && !pMsgHdr.hasOwnProperty("to")) - return false; - - // The names in the user's twitlist have been converted to lowercase for case-insensitive matching. - var fromLower = pMsgHdr.from.toLowerCase(); - var toLower = pMsgHdr.to.toLowerCase(); - var hdrNamesInTwitlist = false; - for (var i = 0; i < this.userSettings.twitList.length && !hdrNamesInTwitlist; ++i) - hdrNamesInTwitlist = (this.userSettings.twitList[i] == fromLower || this.userSettings.twitList[i] == toLower); - return hdrNamesInTwitlist; -} - -/////////////////////////////////////////////////////////////////////////////////// -// Helper functions - -// Displays the program information. -function DisplayProgramInfo() -{ - displayTextWithLineBelow("Digital Distortion Message Reader", true, "\x01n\x01c\x01h", "\x01k\x01h") - console.center("\x01n\x01cVersion \x01g" + READER_VERSION + " \x01w\x01h(\x01b" + READER_DATE + "\x01w)"); - console.crlf(); -} - -// This function returns an array of default colors used in the -// DigDistMessageReader class. -function getDefaultColors() -{ - return { - // Colors for the message header displayed above messages in the scrollable reader mode - msgHdrMsgNumColor: "\x01n\x01b\x01h", // Message # - msgHdrFromColor: "\x01n\x01b\x01h", // From username - msgHdrToColor: "\x01n\x01b\x01h", // To username - msgHdrToUserColor: "\x01n\x01g\x01h", // To username when it's to the current user - msgHdrSubjColor: "\x01n\x01b\x01h", // Message subject - msgHdrDateColor: "\x01n\x01b\x01h", // Message date - - // Message list header line: "Current msg group:" - msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01c", // Normal cyan on blue background - // msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01w", // Normal white on blue background - - // Message list header line: Message group name - msgListHeaderMsgGroupNameColor: "\x01h\x01c", // High cyan - // msgListHeaderMsgGroupNameColor: "\x01h\x01w", // High white - - // Message list header line: "Current sub-board:" - msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01c", // Normal cyan on blue background - // msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01w", // Normal white on blue background - - // Message list header line: Message sub-board name - msgListHeaderMsgSubBoardName: "\x01h\x01c", // High cyan - // msgListHeaderMsgSubBoardName: "\x01h\x01w", // High white - // Line with column headers - // msgListColHeader: "\x01h\x01w", // High white (keep blue background) - msgListColHeader: "\x01n\x01h\x01w", // High white on black background - // msgListColHeader: "\x01h\x01c", // High cyan (keep blue background) - // msgListColHeader: "\x01" + "4\x01h\x01y", // High yellow (keep blue background) - - // Message list information - msgListMsgNumColor: "\x01n\x01h\x01y", - msgListFromColor: "\x01n\x01c", - msgListToColor: "\x01n\x01c", - msgListSubjectColor: "\x01n\x01c", - msgListScoreColor: "\x01n\x01c", - msgListDateColor: "\x01h\x01b", - msgListTimeColor: "\x01h\x01b", - // Message information for messages written to the user - msgListToUserMsgNumColor: "\x01n\x01h\x01y", - msgListToUserFromColor: "\x01h\x01g", - msgListToUserToColor: "\x01h\x01g", - msgListToUserSubjectColor: "\x01h\x01g", - msgListToUserScoreColor: "\x01h\x01g", - msgListToUserDateColor: "\x01h\x01b", - msgListToUserTimeColor: "\x01h\x01b", - // Message information for messages from the user - msgListFromUserMsgNumColor: "\x01n\x01h\x01y", - msgListFromUserFromColor: "\x01n\x01c", - msgListFromUserToColor: "\x01n\x01c", - msgListFromUserSubjectColor: "\x01n\x01c", - msgListFromUserScoreColor: "\x01n\x01c", - msgListFromUserDateColor: "\x01h\x01b", - msgListFromUserTimeColor: "\x01h\x01b", - - // Message list highlight colors - msgListHighlightBkgColor: "\x014", // Background - msgListMsgNumHighlightColor: "\x01h\x01y", - msgListFromHighlightColor: "\x01h\x01c", - msgListToHighlightColor: "\x01h\x01c", - msgListSubjHighlightColor: "\x01h\x01c", - msgListScoreHighlightColor: "\x01h\x01c", - msgListDateHighlightColor: "\x01h\x01w", - msgListTimeHighlightColor: "\x01h\x01w", - - // Lightbar message list help line colors - lightbarMsgListHelpLineBkgColor: "\x017", // Background - lightbarMsgListHelpLineGeneralColor: "\x01b", - lightbarMsgListHelpLineHotkeyColor: "\x01r", - lightbarMsgListHelpLineParenColor: "\x01m", - - // Continue prompt colors - tradInterfaceContPromptMainColor: "\x01n\x01g", // Main text color - tradInterfaceContPromptHotkeyColor: "\x01h\x01c", // Hotkey color - tradInterfaceContPromptUserInputColor: "\x01h\x01g", // User input color - - // Message body color - msgBodyColor: "\x01n\x01w", - - // Read message confirmation colors - readMsgConfirmColor: "\x01n\x01c", - readMsgConfirmNumberColor: "\x01h\x01c", - // Prompt for continuing to list messages after reading a message - afterReadMsg_ListMorePromptColor: "\x01n\x01c", - - // Help screen text color - tradInterfaceHelpScreenColor: "\x01n\x01h\x01w", - - // Colors for choosing a message group & sub-board - areaChooserMsgAreaNumColor: "\x01n\x01w\x01h", - areaChooserMsgAreaDescColor: "\x01n\x01c", - areaChooserMsgAreaNumItemsColor: "\x01b\x01h", - areaChooserMsgAreaHeaderColor: "\x01n\x01y\x01h", - areaChooserSubBoardHeaderColor: "\x01n\x01g", - areaChooserMsgAreaMarkColor: "\x01g\x01h", - areaChooserMsgAreaLatestDateColor: "\x01n\x01g", - areaChooserMsgAreaLatestTimeColor: "\x01n\x01m", - // Highlighted colors (for lightbar mode) - areaChooserMsgAreaBkgHighlightColor: "\x014", // Blue background - areaChooserMsgAreaNumHighlightColor: "\x01w\x01h", - areaChooserMsgAreaDescHighlightColor: "\x01c", - areaChooserMsgAreaDateHighlightColor: "\x01w\x01h", - areaChooserMsgAreaTimeHighlightColor: "\x01w\x01h", - areaChooserMsgAreaNumItemsHighlightColor: "\x01w\x01h", - // Lightbar area chooser help line - lightbarAreaChooserHelpLineBkgColor: "\x017", // Background - lightbarAreaChooserHelpLineGeneralColor: "\x01b", - lightbarAreaChooserHelpLineHotkeyColor: "\x01r", - lightbarAreaChooserHelpLineParenColor: "\x01m", - - // Scrollbar background and scroll block colors (for the enhanced - // message reader interface) - scrollbarBGColor: "\x01n\x01h\x01k", - scrollbarScrollBlockColor: "\x01n\x01h\x01w", - // Color for the line drawn in the 2nd to last line of the message - // area in the enhanced reader mode before a prompt - enhReaderPromptSepLineColor: "\x01n\x01h\x01g", - // Colors for the enhanced reader help line - enhReaderHelpLineBkgColor: "\x017", - enhReaderHelpLineGeneralColor: "\x01b", - enhReaderHelpLineHotkeyColor: "\x01r", - enhReaderHelpLineParenColor: "\x01m", - - // Message header line colors - hdrLineLabelColor: "\x01n\x01c", - hdrLineValueColor: "\x01n\x01b\x01h", - - // Selected message marker color - selectedMsgMarkColor: "\x01n\x01w\x01h", - - // Unread personal email message marker color - unreadMsgMarkColor: "\x01n\x01w\x01h\x01i", - - // Colors for the indexed mode sub-board menu: - indexMenuHeader: "\x01n\x01w", - indexMenuNewIndicator: "\x01n\x01w", - indexMenuDesc: "\x01n\x01w", - indexMenuTotalMsgs: "\x01n\x01w", - indexMenuNumNewMsgs: "\x01n\x01w", - indexMenuLastPostDate: "\x01b\x01h", - // Highlighted/selected: - indexMenuHighlightBkg: "\x014", - indexMenuNewIndicatorHighlight: "\x01w\x01h", - indexMenuDescHighlight: "\x01w\x01h", - indexMenuTotalMsgsHighlight: "\x01w\x01h", - indexMenuNumNewMsgsHighlight: "\x01w\x01h", - indexMenuLastPostDateHighlight: "\x01w\x01h", - indexMenuSeparatorLine: "\x01b", - indexMenuSeparatorText: "\x01y\x01h", - - // Colors for the indexed mode help line text: - // Background - lightbarIndexedModeHelpLineBkgColor: "\x017", - // Hotkey color - lightbarIndexedModeHelpLineHotkeyColor: "\x01r", - // General text - lightbarIndexedModeHelpLineGeneralColor: "\x01b", - // For ) separating the hotkeys from general text - lightbarIndexedModeHelpLineParenColor: "\x01m" - }; -} - -// 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 = skipsp(pString); //pString.replace(/(^\s*)/gi,""); - if (multiple) - pString = pString.replace(/[ ]{2,}/gi," "); - if (trailing) - pString = truncsp(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 : "\x01n\x01w"); - var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\x01n\x01k\x01h"); - - // 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 += HORIZONTAL_SINGLE; - 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(HORIZONTAL_SINGLE); - console.crlf(); - } -} - -// 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 defined by KEY_PAGE_UP or KEY_PAGE_DOWN, -// respectively. Also, F1-F5 will be returned as "\x01F1" -// through "\x01F5", 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 = "\x01F1"; - break; - case 'Q': - userInput = "\x01F2"; - break; - case 'R': - userInput = "\x01F3"; - break; - case 'S': - userInput = "\x01F4"; - break; - case 't': - userInput = "\x01F5"; - 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 = { - grpIdx: pStartGrpIdx, - subIdx: pStartSubIdx, - subCode: msg_area.grp_list[pStartGrpIdx].sub_list[pStartSubIdx].code, - 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_index(true, msgIdx, false); - if ((msgHdr != null) && (((msgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs())) - ++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 -// pFromLeft: Optional boolean - Whether to start from the left (default) or -// from the right. Defaults to true. -// -// Return value: The shortened version of the string -function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft) -{ - if (typeof(pStr) != "string") - return ""; - if (typeof(pNewLength) != "number") - return pStr; - if (pNewLength >= console.strlen(pStr)) - return pStr; - - var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true); - var strCopy = ""; - var tmpStr = ""; - var strIdx = 0; - var lengthGood = true; - if (fromLeft) - { - while (lengthGood && (strIdx < pStr.length)) - { - tmpStr = strCopy + pStr.charAt(strIdx++); - if (console.strlen(tmpStr) <= pNewLength) - strCopy = tmpStr; - else - lengthGood = false; - } - } - else - { - strIdx = pStr.length - 1; - while (lengthGood && (strIdx >= 0)) - { - tmpStr = pStr.charAt(strIdx--) + strCopy; - if (console.strlen(tmpStr) <= pNewLength) - strCopy = tmpStr; - else - lengthGood = false; - } - } - return strCopy; -} - -// Returns whether a given name or CRC16 value matches the logged-in user's -// handle, alias, or name. -// -// Parameters: -// pNameOrCRC16: A name (string) to match against the logged-in user, or a CRC16 (number) -// 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(pNameOrCRC16) -{ - var checkByCRC16 = (typeof(pNameOrCRC16) === "number"); - if (!checkByCRC16 && typeof(pNameOrCRC16) !== "string") - return false; - - var userMatch = false; - if (checkByCRC16) - { - if (user.handle.length > 0) - { - if (userHandleAliasNameMatch.userHandleCRC16 === undefined) - userHandleAliasNameMatch.userHandleCRC16 = crc16_calc(user.handle.toLowerCase()); - userMatch = (userHandleAliasNameMatch.userHandleCRC16 == pNameOrCRC16); - } - if (!userMatch && (user.alias.length > 0)) - { - if (userHandleAliasNameMatch.userAliasCRC16 === undefined) - userHandleAliasNameMatch.userAliasCRC16 = crc16_calc(user.alias.toLowerCase()); - userMatch = (userHandleAliasNameMatch.userAliasCRC16 == pNameOrCRC16); - } - if (!userMatch && (user.name.length > 0)) - { - if (userHandleAliasNameMatch.userNameCRC16 === undefined) - userHandleAliasNameMatch.userNameCRC16 = crc16_calc(user.name.toLowerCase()); - userMatch = (userHandleAliasNameMatch.userNameCRC16 == pNameOrCRC16); - } - } - else - { - if (pNameOrCRC16 != "") - { - var nameUpper = pNameOrCRC16.toUpperCase(); - // If the name starts & ends with the same quote character, then remove the - // quote characters. - var firstChar = nameUpper.charAt(0); - var lastChar = nameUpper.charAt(nameUpper.length-1); - if ((firstChar == "\"" && lastChar == "\"") ||(firstChar == "'" && lastChar == "'")) - nameUpper = nameUpper.substring(1, nameUpper.length-1); - - if (user.handle.length > 0) - { - if (userHandleAliasNameMatch.userHandleUpper === undefined) - userHandleAliasNameMatch.userHandleUpper = user.handle.toUpperCase(); - userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userHandleUpper) > -1); - } - if (!userMatch && (user.alias.length > 0)) - { - if (userHandleAliasNameMatch.userAliasUpper === undefined) - userHandleAliasNameMatch.userAliasUpper = user.alias.toUpperCase(); - userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userAliasUpper) > -1); - } - if (!userMatch && (user.name.length > 0)) - { - if (userHandleAliasNameMatch.userNameUpper === undefined) - userHandleAliasNameMatch.userNameUpper = user.name.toUpperCase(); - userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userNameUpper) > -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 -// pUseScrollbar: Boolean - Whether or not to display the scrollbar. If false, -// this will display a scroll status line at the bottom instead. -// 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). -// pmode: Optional - Print mode (important for UTF8 info) -// -// 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, pUseScrollbar, pScrollUpdateFn, - pmode) -{ - // 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 = { - lastKeypress: "", - topLineIdx: pTopLineIdx - }; - - // Create an array of color/attribute codes for each line of - // text, in case there are any such codes in the text lines, - // so that the colors in the message are displayed properly. - // First, get the last color/attribute codes from first text - // line and apply them to the next line, and so on. - var attrCodes = getAttrsBeforeStrIdx(pTxtLines[0], pTxtLines[0].length-1); - for (var lineIdx = 1; lineIdx < pTxtLines.length; ++lineIdx) - { - pTxtLines[lineIdx] = attrCodes + pTxtLines[lineIdx]; - attrCodes = getAttrsBeforeStrIdx(pTxtLines[lineIdx], pTxtLines[lineIdx].length-1); - } - - var pMode = (typeof(pmode) === "number" ? pmode|P_NOATCODES : P_NOATCODES); - var writeTxtLines = pWriteTxtLines; - var continueOn = true; - var mouseInputOnly_continue = false; - while (continueOn) - { - mouseInputOnly_continue = false; - - // 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 (pUseScrollbar && 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], pMode); - printf("\x01n%*s", pWidth-console.strlen(pTxtLines[lineIdx]), ""); - } - // If there are still some lines left in the message reading area, then - // clear the lines. - console.print("\x01n" + 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); - if (!continueOn) - break; - - 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_UP: // Previous page - if (retObj.topLineIdx > 0) - { - retObj.topLineIdx -= pHeight; - if (retObj.topLineIdx < 0) - retObj.topLineIdx = 0; - 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_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; -} - -// Gets all the attribute codes from an array of text lines until a certain index. -// -// Parameters: -// pTxtLines: The array of text lines of the message being displayed -// pEndArrayIdx: One past the last edit line index to get attributes for -// pIncludeEndArrayIdxAttrs: Optional boolean: Whether or not to include the attributes for the line at the end array index. -// Defaults to false. -// pLastLineTextEndIdx: Optional - Only used when pIncludeEndArrayIdxAttrs is true, this parameter specifies the -// end index (non-inclusive) in the last text line to include attributes for. If not specified, -// the entire end line will be used to include its attributes. -// -// Return value: A string containing the relevant attribute codes to apply up to the given line index (non-inclusive). -function getAllEditLineAttrsUntilLineIdx(pTxtLines, pEndArrayIdx, pIncludeEndArrayIdxAttrs, pLastLineTextEndIdx) -{ - if (typeof(pTxtLines) !== "object" || typeof(pEndArrayIdx) !== "number" || pEndArrayIdx < 0) - return ""; - - var includeEndArrayIdxAttrs = (typeof(pIncludeEndArrayIdxAttrs) === "boolean" ? pIncludeEndArrayIdxAttrs : false); - - var syncAttrRegex = /\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]/gi; - var attributesStr = ""; - var onePastLastIdx = (includeEndArrayIdxAttrs ? pEndArrayIdx + 1 : pEndArrayIdx); - for (var i = 0; i < onePastLastIdx; ++i) - { - if (typeof(pTxtLines[i]) !== "string") continue; - var attrCodes; - if (typeof(pLastLineTextEndIdx) === "number" && i === (onePastLastIdx-1)) - { - // Note: dd_lightbar_menu.js defines substrWithAttrCodes(pStr, pStartIdx, pLen) - var textLine = substrWithAttrCodes(pTxtLines[i], 0, pLastLineTextEndIdx); - attrCodes = textLine.match(syncAttrRegex); - } - else - attrCodes = pTxtLines[i].match(syncAttrRegex); - if (attrCodes != null) - { - for (var attrMatchI = 0; attrMatchI < attrCodes.length; ++attrMatchI) - attributesStr += attrCodes[attrMatchI]; - } - } - // If there is a normal attribute code in the middle of the string, remove anything before it - var normalAttrIdx = attributesStr.lastIndexOf("\x01n"); - if (normalAttrIdx < 0) - normalAttrIdx = attributesStr.lastIndexOf("\x01N"); - if (normalAttrIdx > -1) - attributesStr = attributesStr.substr(normalAttrIdx/*+2*/); - return attributesStr; -} - -// 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 -// 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: Optional: The starting message index (0-based). Defaults to 0. -// pEndIndex: Optional: One past the last message index. Defaults to the total number -// of messages. -// -// pUserNum: Optional: The user number (for reading personal email) -// -// Return value: An object with the following arrays: -// indexed: A 0-based indexed array of message headers -function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEmailFromUser, pStartIndex, pEndIndex, pUserNum) -{ - var msgHeaders = { - indexed: [] - }; - if ((pSubCode != "mail") && ((typeof(pSearchString) != "string") || !searchTypePopulatesSearchResults(pSearchType))) - return msgHeaders; - - var msgbase = new MsgBase(pSubCode); - if (!msgbase.open()) - return msgHeaders; - - var startMsgIndex = 0; - var endMsgIndex = msgbase.total_msgs; - if (typeof(pStartIndex) == "number") - { - if ((pStartIndex >= 0) && (pStartIndex < msgbase.total_msgs)) - startMsgIndex = pStartIndex; - } - if (typeof(pEndIndex) == "number") - { - if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= msgbase.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; - var getAllMsgHdrs = false; - 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(false, pMsgHdr.number, false, false, true, true)); - return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr, pUserNum); - } - } - else - { - // We're reading mail to the user - matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { - var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true)); - var msgMatchesCriteria = (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr, pUserNum)); - // If only new/unread personal email is to be displayed, then check - // that the message has not been read. - if (gCmdLineArgVals.onlynewpersonalemail) - msgMatchesCriteria = (msgMatchesCriteria && ((pMsgHdr.attr & MSG_READ) == 0)); - return msgMatchesCriteria; - } - } - } - break; - case SEARCH_KEYWORD: - getAllMsgHdrs = true; - matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { - var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true)); - 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: - getAllMsgHdrs = true; - 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: - getAllMsgHdrs = true; - matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) { - return (pMsgHdr.to.toUpperCase() == pSearchStr); - } - break; - case SEARCH_TO_USER_CUR_MSG_AREA: - getAllMsgHdrs = true; - 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) || canViewDeletedMsgs()) && userHandleAliasNameMatch(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 = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].last_read); - startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, GetScanPtrOrLastMsgNum(pSubCode)); - if (startMsgIndex == -1) - { - msg_area.sub[pSubCode].scan_ptr = 0; - startMsgIndex = 0; - } - else - { - // If this message has been read, then start at the next message. - var startMsgHeader = msgbase.get_msg_index(true, startMsgIndex, false); - if (startMsgHeader == null) - ++startMsgIndex; - else - { - if ((startMsgHeader.attr & MSG_READ) == MSG_READ) - ++startMsgIndex; - } - } - } - if (typeof(pEndIndex) != "number") - endMsgIndex = (msgbase.total_msgs > 0 ? msgbase.total_msgs : 0); - */ - // For the new-to-you scan faster, check messages to the user from the user's new scan pointer to - // messagebase.last_msg - // scan_ptr (not last_read?) - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number") - startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].scan_ptr); - else if (typeof(pStartIndex) === "number") - startMsgIndex = pStartIndex; - else - startMsgIndex = 0; - endMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msgbase.last_msg) + 1; - if (endMsgIndex == 0) // Not valid - endMsgIndex = msgbase.total_msgs; - } - 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) || canViewDeletedMsgs()) && ((pMsgHdr.attr & MSG_READ) == 0) && userHandleAliasNameMatch(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_index(false, msg_area.sub[pSubBoardCode].last_read, false); - var lastReadMsgOffset = 0; - if (lastReadMsgHdr != null) - lastReadMsgOffset = absMsgNumToIdx(pSubBoardCode, lastReadMsgHdr.number); - if (lastReadMsgOffset < 0) - lastReadMsgOffset = 0; - return (absMsgNumToIdx(pSubBoardCode, pMsgHdr.number) > lastReadMsgOffset); - } - */ - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number") - startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].scan_ptr); - else if (typeof(pStartIndex) === "number") - startMsgIndex = pStartIndex; - else - startMsgIndex = 0; - endMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msgbase.last_msg) + 1; - // TODO: Is this matchFn really needed? - 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_index(false, msg_area.sub[pSubBoardCode].last_read, false); - var lastReadMsgOffset = 0; - if (lastReadMsgHdr != null) - lastReadMsgOffset = absMsgNumToIdx(pSubBoardCode, lastReadMsgHdr.number); - if (lastReadMsgOffset < 0) - lastReadMsgOffset = 0; - return (absMsgNumToIdx(pSubBoardCode, pMsgHdr.number) > lastReadMsgOffset); - } - break; - } - // Search the messages - if (matchFn != null) - { - // If we want to use get_all_msg_headers, then use it. Otherwise, - // iterate through all message offsets and get the headers. We want to - // use get_all_msg_hdrs() if possible because that will include information - // about how many votes each message got (up/downvotes for regular - // messages or who voted for which options for poll messages). - if (getAllMsgHdrs) - { - // Pass false to get_all_msg_headers() to tell it not to include vote messages - // (the parameter was introduced in Synchronet 3.17+) - var tmpHdrs = msgbase.get_all_msg_headers(false); - // Re-do startMsgIndex and endMsgIndex based on the message headers we got - startMsgIndex = 0; - endMsgIndex = msgbase.total_msgs; - if (typeof(pStartIndex) == "number") - { - if ((pStartIndex >= 0) && (pStartIndex < tmpHdrs.length)) - startMsgIndex = pStartIndex; - } - if (typeof(pEndIndex) == "number") - { - if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= tmpHdrs.length)) - endMsgIndex = pEndIndex; - } - // Search the message headers - var msgIdx = 0; - for (var prop in tmpHdrs) - { - // Only add the message header if the message is readable to the user - // and msgIdx is within bounds - if ((msgIdx >= startMsgIndex) && (msgIdx < endMsgIndex) && isReadableMsgHdr(tmpHdrs[prop], pSubCode)) - { - if (tmpHdrs[prop] != null) - { - if (matchFn(pSearchString, tmpHdrs[prop], msgbase, pSubCode)) - msgHeaders.indexed.push(tmpHdrs[prop]); - } - } - ++msgIdx; - } - } - else - { - for (var msgIdx = startMsgIndex; msgIdx < endMsgIndex; ++msgIdx) - { - var msgHeader = msgbase.get_msg_header(true, msgIdx, false); - if (msgHeader != null) - { - if (matchFn(pSearchString, msgHeader, msgbase, pSubCode)) - msgHeaders.indexed.push(msgHeader); - } - } - } - } - else - { - // There is no match function - Get all message headers between the start & end indexes - } - msgbase.close(); - return msgHeaders; -} - -// Tries to look up a message index (offset) with a given message number. -// 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 absMsgNumToIdxWithMsgbaseObj(pMsgbase, pMsgNum) -{ - if ((pMsgbase == null) || (typeof(pMsgbase) != "object")) - return -1; - if (!pMsgbase.is_open) - return -1; - - if (typeof(pMsgNum) != "number") - return -1; - - var messageIdx = 0; - // If pMsgNum is the 'last number' special value, then use the last message - // number in the messagebase. - if (msgNumIsLatestMsgSpecialVal(pMsgNum)) - messageIdx = pMsgbase.total_msgs - 1; // Or this.NumMessages() - 1 but can't because this isn't a class member function - else - { - var msgHdr = pMsgbase.get_msg_index(false, pMsgNum, false); - 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."); - } - messageIdx = (msgHdr != null ? msgHdr.offset : -1); - } - return messageIdx; -} -// Tries to look up a message index (offset) with a given message number. -// On error, returns -1. -// -// Parameters: -// pSubCodeOrMsgbase: The internal sub-board code of the messagebase to check, or -// an already-open messagebase object -// pMsgNum: The absolute message number -// -// Return value: The message's index, or -1 on error. -function absMsgNumToIdx(pSubCodeOrMsgbase, pMsgNum) -{ - var typename = typeof(pSubCodeOrMsgbase); - if (typename === "string") - { - var msgOffset = -1; - var msgbase = new MsgBase(pSubCodeOrMsgbase); - if (msgbase.open()) - { - msgOffset = absMsgNumToIdxWithMsgbaseObj(msgbase, pMsgNum); - msgbase.close(); - } - return msgOffset; - } - else if (typename === "object") - return absMsgNumToIdxWithMsgbaseObj(msgbase, pMsgNum); -} - -// 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_index(true, pMsgIdx, false); - 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); -} - -// 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 -// pUserNum: Optional - A user number to match with. If not specified, this will default to -// the current logged-in user number. -// -// Return value: Boolean - Whether or not the message is to the user and is not -// deleted. -function msgIsToUserByNum(pMsgHdr, pUserNum) -{ - if (typeof(pMsgHdr) !== "object") - return false; - // Return false if the message is marked as deleted and the user can't read deleted messages - if (((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs()) - return false; - - var userNum = user.number; - if (user.is_sysop && typeof(pUserNum) === "number" && pUserNum > 0 && pUserNum <= system.lastuser) - userNum = pUserNum; - - 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 == userNum); - return msgIsToUser; -} - -// Returns whether or not a message header is to the current logged-in user by name, alias, or handle -function msgIsToCurrentUserByName(pMsgHdrOrIdx) -{ - if (typeof(pMsgHdrOrIdx) !== "object" || !pMsgHdrOrIdx.hasOwnProperty("to")) - return false; - // Return false if the message is marked as deleted and the user can't read deleted messages - if (((pMsgHdrOrIdx.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs()) - return false; - - return userHandleAliasNameMatch(pMsgHdrOrIdx.to); -} - -// Checks to see if there are any unread messages to the current -// logged-in user in a sub-board -// -// Parameters: -// pMsgbase: An opened MessageBase object for the sub-board -// pSubCode: The internal code of the sub-board of the opened messagebase -// -// Return value: Boolean - Whether or not there are any unread messages to the current logged-in user -function anyUnreadMsgsToUserWithMsgbase(pMsgbase, pSubCode) -{ - if (typeof(pMsgbase) !== "object") - return false; - - var unreadMsgsToUserFound = false; - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number" && !msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr)) - { - var startMsgIdx = absMsgNumToIdxWithMsgbaseObj(pMsgbase, msg_area.sub[pSubCode].scan_ptr); - var endMsgIdx = absMsgNumToIdxWithMsgbaseObj(pMsgbase, pMsgbase.last_msg) + 1; - if (endMsgIdx == 0) // Not valid - endMsgIdx = pMsgbase.total_msgs; - // Check for any messages to the user between the start & end indexes - for (var i = startMsgIdx; i < endMsgIdx; ++i) - { - var msgHeader = pMsgbase.get_msg_index(true, i, false); - if (msgHeader != null && (msgHeader.attr & MSG_READ) == 0 && msgIsToCurrentUserByName(msgHeader)) - { - unreadMsgsToUserFound = true; - break; - } - } - } - return unreadMsgsToUserFound; -} -// Checks to see if there are any unread messages to the current -// logged-in user in a sub-board -// -// Parameters: -// pSuBCode: The internal code of the sub-board to check -// -// Return value: Boolean - Whether or not there are any unread messages to the current logged-in user -function anyUnreadMsgsToUser(pSubCode) -{ - var unreadMsgsToUserFound = false; - var msgbase = new MsgBase(pSubCode); - if (msgbase.open()) - { - unreadMsgsToUserFound = anyUnreadMsgsToUserWithMsgbase(msgbase, pSubCode); - msgbase.close(); - } - return unreadMsgsToUserFound; -} - -// 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 -// pUserNum: Optional - A user number to match with. If not specified, this will default to -// the current logged-in user number. -// -// Return value: Boolean - Whether or not the message is from the logged-in user -// and is not deleted. -function msgIsFromUser(pMsgHdr, pUserNum) -{ - if (typeof(pMsgHdr) != "object") - return false; - // Return false if the message is marked as deleted and the user can't read deleted messages - if (Boolean(pMsgHdr.attr & MSG_DELETE) && !canViewDeletedMsgs()) - return false; - - var pUserNumIsValid = (typeof(pUserNum) === "number" && pUserNum > 0 && pUserNum <= system.lastuser); - - 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 == (pUserNumIsValid && user.is_sysop ? pUserNum : 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 - { - if (pUserNumIsValid) - { - var theUser = new User(pUserNum); - isFromUser = ((hdrFromUpper == theUser.alias.toUpperCase()) || (hdrFromUpper == theUser.name.toUpperCase())); - } - else - isFromUser = ((hdrFromUpper == user.alias.toUpperCase()) || (hdrFromUpper == user.name.toUpperCase())); - } - } - - return isFromUser; -} - -// 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); -} - -// With a MsgBase object, counts the number of readable messages starting -// with (and including) a message number -// -// Parameters: -// pMsgbase: A MsgBase object (should be open) -// pSubCode: The internal code of the sub-board -// pMsgNum: A message number to start at -// -// Return value: The number of readable messages starting at the given message number -function numReadableMsgsFromAbsMsgNumWithMsgbase(pMsgbase, pSubCode, pMsgNum) -{ - var numReadableMsgs = 0; - if (pMsgbase.is_open) - { - var totalNumMsgs = pMsgbase.total_msgs; - var msgIdx = absMsgNumToIdx(pMsgbase, pMsgNum); - for (; i < totalNumMsgs; ++msgIdx) - { - if (isReadableMsgHdr(pMsgbase.get_msg_index(true, msgIdx), pSubCode)) - ++numReadableMsgs; - } - } - return numReadableMsgs; -} - -// 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: -// argv: 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(argv) -{ - var argVals = getDefaultArgParseObj(); - - // Sanity checking for argv - Make sure it's an array - if (!Array.isArray(argv)) - return argVals; - - // First, test the arguments to see if they're in a format as called by - // Synchronet for loadable modules - argVals = parseLoadableModuleArgs(argv); - if (argVals.loadableModule || argVals.continuousNewScan || argVals.newScanBack) - return argVals; - - // Go through argv 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 < argv.length; ++i) - { - // We're looking for strings that start with "-", except strings that are - // only "-". - if ((typeof(argv[i]) != "string") || (argv[i].length == 0) || - (argv[i].charAt(0) != "-") || (argv[i] == "-")) - { - continue; - } - - // Look for an = and if found, split the string on the = - equalsIdx = argv[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 = argv[i].substring(1, equalsIdx).toLowerCase(); - argVal = argv[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 = argv[i].substr(1).toLowerCase(); - if ((argName == "chooseareafirst") || (argName == "personalemail") || - (argName == "personalemailsent") || (argName == "allpersonalemail") || - (argName == "verboselogging") || (argName == "suppresssearchtypetext") || - (argName == "onlynewpersonalemail") || (argName == "indexedmode")) - { - 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) - { - argVals.subboard = "mail"; - // 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; -} -// Helper for parseArgs() - If we get loadable module arguments from Synchronet, this parses them. -// -// Parameters: -// argv: An array of strings containing values in the format -arg=val -// -// Return value: An object containing the argument values. The property "loadableModule" -// in this object will be a boolean that specifies whether or not loadable -// module arguments were specified. -function parseLoadableModuleArgs(argv) -{ - // TODO: Allow indexed reader mode? - //argVals.indexedmode = true; - - var argVals = getDefaultArgParseObj(); - - var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits - var arg1Lower = argv[0].toLowerCase(); - // 2 args, and the 1st arg is a sub-board code & the 2nd arg is numeric & is - // the value of SCAN_INDEX: List messages in the specified sub-board (List Msgs module) - if (argv.length == 2 && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && +(argv[1]) === SCAN_INDEX) - { - argVals.loadableModule = true; - argVals.subboard = arg1Lower; - argVals.startmode = "list"; - } - // 2 parameters: Whether or not all subs are being scanned (0 or 1), and the scan mode (numeric) - // (Scan Subs module) - else if (argv.length == 2 && /^[0-1]$/.test(argv[0]) && allDigitsRegex.test(argv[1])) - { - argVals.loadableModule = true; - var scanAllSubs = (argv[0] == "1"); - var scanMode = +(argv[1]); - //if ((scanMode & SCAN_NEW) == SCAN_NEW) - if (Boolean(scanMode & SCAN_NEW)) - { - // Newscan - argVals.search = "new_msg_scan"; - argVals.suppresssearchtypetext = true; - if (scanAllSubs) - argVals.search = "new_msg_scan_all"; - // TODO: SCAN_CONT and SCAN_BACK could be used along with SCAN_NEW - // SCAN_CONT: Continuous message scanning - // SCAN_BACK: Display most recent message if none new - if (Boolean(scanMode & SCAN_CONT) || Boolean(scanMode & SCAN_BACK)) - { - // Stock Synchronet functionality for continuous & back newscan - bbs.scan_subs(scanMode, scanAllSubs); - argVals.exitNow = true; - } - } - //else if (((scanMode & SCAN_TOYOU) == SCAN_TOYOU) || ((scanMode & SCAN_UNREAD) == SCAN_UNREAD)) - else if (Boolean(scanMode & SCAN_TOYOU) || Boolean(scanMode & SCAN_UNREAD)) - { - // Scan for messages posted to you/new messages posted to you - argVals.startmode = "read"; - argVals.search = "to_user_new_scan"; - argVals.suppresssearchtypetext = true; - if (scanAllSubs) - argVals.search = "to_user_new_scan_all"; - } - else if (Boolean(scanMode & SCAN_FIND)) - { - argVals.search = "keyword_search"; - argVals.startmode = "list"; - } - else - { - // Stock Synchronet functionality. Includes SCAN_CONT and SCAN_BACK. - bbs.scan_subs(scanMode, scanAllSubs); - argVals.exitNow = true; - } - } - // Scan Msgs loadable module support: - // 1. The sub-board internal code - // 2. The scan mode (numeric) - // 3. Optional: Search text (if any) - else if ((argv.length == 2 || argv.length == 3) && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1])) - { - argVals.loadableModule = true; - var scanMode = +(argv[1]); - if (scanMode == SCAN_READ) - { - argVals.subboard = arg1Lower; - argVals.startmode = "read"; - // If a search string is specified (as the 3rd command-line argument), - // then use it for a search scan. - if (argv.length == 3 && argv[2] != "") - { - argVals.search = "keyword_search"; - argVals.searchtext = argv[2]; - } - } - else if (scanMode == SCAN_FIND) - { - argVals.subboard = arg1Lower; - argVals.search = "keyword_search"; - argVals.startmode = "list"; - if (argv.length == 3 && argv[2] != "") - argVals.searchtext = argv[2]; - } - // Some modes that the Digital Distortion Message Reader doesn't handle yet: Use - // Synchronet's stock behavior. - else - { - if (argv.length == 3) - bbs.scan_msgs(arg1Lower, scanMode, argv[2]); - else - bbs.scan_msgs(arg1Lower, scanMode); - argVals.exitNow = true; - } - } - // Reading personal email: 'Which' mailbox & user number (both numeric) (Read Mail module) - else if ((argv.length == 2 || argv.length == 3) && allDigitsRegex.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidUserNum(+(argv[1]))) - { - argVals.loadableModule = true; - var whichMailbox = +(argv[0]); - var userNum = +(argv[1]); - // The optional 3rd argument in this case is mode bits. See if we should only display - // new (unread) personal email. - var newMailOnly = false; - if (argv.length >= 3) - { - var modeVal = +(argv[2]); - newMailOnly = (((modeVal & SCAN_FIND) == SCAN_FIND) && ((modeVal & LM_UNREAD) == LM_UNREAD)); - } - // Start in list mode - argVals.startmode = "list"; // "read" - // Note: MAIL_ANY won't be passed to this script. - switch (whichMailbox) - { - case MAIL_YOUR: // Mail sent to you - argVals.personalemail = true; - argVals.usernum = +(argv[1]); - if (newMailOnly) - argVals.onlynewpersonalemail = true; - break; - case MAIL_SENT: // Mail you have sent - argVals.personalemailsent = true; - argVals.usernum = +(argv[1]); - break; - case MAIL_ALL: - argVals.allpersonalemail = true; - break; - default: - bbs.read_mail(whichMailbox); - argVals.exitNow = true; - break; - } - } - return argVals; -} -// Returns an object with default settings for argument parsing -function getDefaultArgParseObj() -{ - return { - chooseareafirst: false, - personalemail: false, - onlynewpersonalemail: false, - personalemailsent: false, - verboselogging: false, - suppresssearchtypetext: false, - indexedmode: false, - loadableModule: false, - exitNow: false - }; -} - -// 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. -// -// 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) -{ - if (makeMainMsgAttrStr.attrStrs === undefined) - { - makeMainMsgAttrStr.attrStrs = [ - { attr: MSG_DELETE, str: "Del" }, - { attr: MSG_PRIVATE, str: "Priv" }, - { attr: MSG_READ, str: "Read" }, - { attr: MSG_PERMANENT, str: "Perm" }, - { attr: MSG_LOCKED, str: "Lock" }, - { attr: MSG_ANONYMOUS, str: "Anon" }, - { attr: MSG_KILLREAD, str: "Killread" }, - { attr: MSG_MODERATED, str: "Mod" }, - { attr: MSG_VALIDATED, str: "Valid" }, - { attr: MSG_REPLIED, str: "Repl" }, - { attr: MSG_NOREPLY, str: "NoRepl" } - ]; - } - - var msgAttrStr = ""; - if (typeof(pMainMsgAttrs) == "number") - { - for (var i = 0; i < makeMainMsgAttrStr.attrStrs.length; ++i) - { - if (Boolean(pMainMsgAttrs & makeMainMsgAttrStr.attrStrs[i].attr)) - { - if (msgAttrStr.length > 0) - msgAttrStr += ", "; - msgAttrStr += makeMainMsgAttrStr.attrStrs[i].str; - } - } - } - if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string")) - msgAttrStr = pIfEmptyString; - return msgAttrStr; -} - -// Returns a string describing auxiliary message attributes. -// -// 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) -{ - if (makeAuxMsgAttrStr.attrStrs === undefined) - { - makeAuxMsgAttrStr.attrStrs = [ - { attr: MSG_FILEREQUEST, str: "Freq" }, - { attr: MSG_FILEATTACH, str: "Attach" }, - { attr: MSG_KILLFILE, str: "KillFile" }, - { attr: MSG_RECEIPTREQ, str: "RctReq" }, - { attr: MSG_CONFIRMREQ, str: "ConfReq" }, - { attr: MSG_NODISP, str: "NoDisp" } - ]; - if (typeof(MSG_TRUNCFILE) === "number") - makeAuxMsgAttrStr.attrStrs.push({ attr: MSG_TRUNCFILE, str: "TruncFile" }); - } - - var msgAttrStr = ""; - if (typeof(pAuxMsgAttrs) == "number") - { - for (var i = 0; i < makeAuxMsgAttrStr.attrStrs.length; ++i) - { - if (Boolean(pAuxMsgAttrs & makeAuxMsgAttrStr.attrStrs[i].attr)) - { - if (msgAttrStr.length > 0) - msgAttrStr += ", "; - msgAttrStr += makeAuxMsgAttrStr.attrStrs[i].str; - } - } - } - if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string")) - msgAttrStr = pIfEmptyString; - return msgAttrStr; -} - -// Returns a string describing network message attributes. -// -// 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) -{ - if (makeNetMsgAttrStr.attrStrs === undefined) - { - makeNetMsgAttrStr.attrStrs = [ - { attr: MSG_LOCAL, str: "FromLocal" }, - { attr: MSG_INTRANSIT, str: "Transit" }, - { attr: MSG_SENT, str: "Sent" }, - { attr: MSG_KILLSENT, str: "KillSent" }, - { attr: MSG_ARCHIVESENT, str: "ArcSent" }, - { attr: MSG_HOLD, str: "Hold" }, - { attr: MSG_CRASH, str: "Crash" }, - { attr: MSG_IMMEDIATE, str: "Now" }, - { attr: MSG_DIRECT, str: "Direct" } - ]; - if (typeof(MSG_GATE) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_GATE, str: "Gate" }); - if (typeof(MSG_ORPHAN) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_ORPHAN, str: "Orphan" }); - if (typeof(MSG_FPU) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_FPU, str: "FPU" }); - if (typeof(MSG_TYPELOCAL) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPELOCAL, str: "ForLocal" }); - if (typeof(MSG_TYPEECHO) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPEECHO, str: "ForEcho" }); - if (typeof(MSG_TYPENET) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPENET, str: "ForNetmail" }); - if (typeof(MSG_MIMEATTACH) === "number") - makeNetMsgAttrStr.attrStrs.push({ attr: MSG_MIMEATTACH, str: "MimeAttach" }); - } - - var msgAttrStr = ""; - if (typeof(pNetMsgAttrs) == "number") - { - for (var i = 0; i < makeNetMsgAttrStr.attrStrs.length; ++i) - { - if (Boolean(pNetMsgAttrs & makeNetMsgAttrStr.attrStrs[i].attr)) - { - if (msgAttrStr.length > 0) - msgAttrStr += ", "; - msgAttrStr += makeNetMsgAttrStr.attrStrs[i].str; - } - } - } - 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; -} - -// 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 -// pLogLevel: The log level. Optional - Defaults to LOG_INFO. -function writeToSysAndNodeLog(pMessage, pLogLevel) -{ - if (typeof(pMessage) != "string") - return; - - var logMessage = "Digital Distortion Message Reader (" + user.alias + "): " + pMessage; - var logLevel = (typeof(pLogLevel) == "number" ? pLogLevel : LOG_INFO); - log(logLevel, 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; -} - -// 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) -{ - var fieldTypeLabel = ""; - switch (pFieldListType) - { - case SMB_COMMENT: - fieldTypeLabel = "Comment"; - break - case SMB_POLL_ANSWER: - fieldTypeLabel = "Poll answer"; - break; - case 0x64: // SMB_GROUP - fieldTypeLabel = "Group"; - break; - case FIDOCTRL: - fieldTypeLabel = "FIDO control"; - break; - case FIDOSEENBY: - fieldTypeLabel = "Seen-by"; - break; - case FIDOPATH: - fieldTypeLabel = "FIDO Path"; - break; - case RFC822HEADER: - fieldTypeLabel = "RFCC822 Header"; - break; - case 0xB3: // RFC822TO - fieldTypeLabel = "RFC822 To"; - break; - case 0xB6: // RFC822CC - fieldTypeLabel = "RFC822 CC"; - break; - case 0xB7: // RFC822ORG - fieldTypeLabel = "RFC822 Org"; - break; - case 0xB4: // RFC822FROM - fieldTypeLabel = "RFC822 From"; - break; - case 0xB5: // RFC822REPLYTO - fieldTypeLabel = "RFC822 Reply To"; - break; - case 0xB8: // RFC822SUBJECT - fieldTypeLabel = "RFC822 Subject"; - break; - case SMTPRECEIVED: - fieldTypeLabel = "SMTP Received"; - break; - case 0xF1: // UNKNOWNASCII - fieldTypeLabel = "UNKNOWN (ASCII)"; - 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; -} - -// Loads a text file (an .ans or .asc) into an array. This will first look for -// an .ans version, and if exists, convert to Synchronet colors before loading -// it. If an .ans doesn't exist, this will look for an .asc version. -// -// Parameters: -// pFilenameBase: The filename without the extension -// pMaxNumLines: Optional - The maximum number of lines to load from the text file -// -// Return value: An array containing the lines from the text file -function loadTextFileIntoArray(pFilenameBase, pMaxNumLines) -{ - if (typeof(pFilenameBase) != "string") - return []; - - var maxNumLines = (typeof(pMaxNumLines) == "number" ? pMaxNumLines : -1); - - var txtFileLines = []; - // See if there is a header file that is made for the user's terminal - // width (areaChgHeader-<width>.ans/asc). If not, then just go with - // msgHeader.ans/asc. - var txtFileExists = true; - var txtFilenameFullPath = gStartupPath + pFilenameBase; - var txtFileFilename = ""; - if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".ans")) - txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".ans"; - else if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".asc")) - txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".asc"; - else if (file_exists(txtFilenameFullPath + ".ans")) - txtFileFilename = txtFilenameFullPath + ".ans"; - else if (file_exists(txtFilenameFullPath + ".asc")) - txtFileFilename = txtFilenameFullPath + ".asc"; - else - txtFileExists = false; - if (txtFileExists) - { - var syncConvertedHdrFilename = txtFileFilename; - // If the user's console doesn't support ANSI and the header file is ANSI, - // then convert it to Synchronet attribute codes and read that file instead. - if (!console.term_supports(USER_ANSI) && (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS")) - { - syncConvertedHdrFilename = txtFilenameFullPath + "_converted.asc"; - if (!file_exists(syncConvertedHdrFilename)) - { - if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS") - { - var dotIdx = txtFileFilename.lastIndexOf("."); - if (dotIdx >= 0) - { - var filenameBase = txtFileFilename.substr(0, dotIdx); - var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \"" - + 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 = txtFileFilename; - } - } - /* - // 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 = txtFilenameFullPath + "_converted.asc"; - if (!file_exists(syncConvertedHdrFilename)) - { - if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS") - { - var filenameBase = txtFileFilename.substr(0, dotIdx); - var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \"" - + 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 = txtFileFilename; - } - */ - // Read the header file into txtFileLines - var hdrFile = new File(syncConvertedHdrFilename); - if (hdrFile.open("r")) - { - var fileLine = null; - while (!hdrFile.eof) - { - // 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); - txtFileLines.push(fileLine); - - // If the header array now has the maximum number of lines, then - // stop reading the header file. - if (txtFileLines.length == maxNumLines) - break; - } - hdrFile.close(); - } - } - return txtFileLines; -} - -// Returns the portion (if any) of a string after the period. -// -// Parameters: -// pStr: The string to extract from -// -// Return value: The portion of the string after the dot, if there is one. If -// not, then an empty string will be returned. -function getStrAfterPeriod(pStr) -{ - var strAfterPeriod = ""; - var dotIdx = pStr.lastIndexOf("."); - if (dotIdx > -1) - strAfterPeriod = pStr.substr(dotIdx+1); - return strAfterPeriod; -} - -// Adjusts a message's when-written time to the BBS's local time. -// -// Parameters: -// pMsgHdr: A message header object -// -// Return value: The message's when_written_time adjusted to the BBS's local time. -// If the message header doesn't have a when_written_time or -// when_written_zone property, then this function will return -1. -function msgWrittenTimeToLocalBBSTime(pMsgHdr) -{ - if (!pMsgHdr.hasOwnProperty("when_written_time") || !pMsgHdr.hasOwnProperty("when_written_zone_offset") || !pMsgHdr.hasOwnProperty("when_imported_zone_offset")) - return -1; - - var timeZoneDiffMinutes = pMsgHdr.when_imported_zone_offset - pMsgHdr.when_written_zone_offset; - //var timeZoneDiffMinutes = pMsgHdr.when_written_zone - system.timezone; - var timeZoneDiffSeconds = timeZoneDiffMinutes * 60; - var msgWrittenTimeAdjusted = pMsgHdr.when_written_time + timeZoneDiffSeconds; - return msgWrittenTimeAdjusted; -} - -// Returns a string containing the message group & sub-board numbers and -// descriptions. -// -// Parameters: -// pMsgbase: A MsgBase object -// -// Return value: A string containing the message group & sub-board numbers and -// descriptions -function getMsgAreaDescStr(pMsgbase) -{ - if (typeof(pMsgbase) != "object") - return ""; - if (!pMsgbase.is_open) - return ""; - - var descStr = ""; - if (pMsgbase.cfg != null) - { - descStr = format("Group/sub-board num: %d, %d; %s - %s", pMsgbase.cfg.grp_number, - pMsgbase.subnum, msg_area.grp_list[pMsgbase.cfg.grp_number].description, - pMsgbase.cfg.description); - } - else - { - if ((pMsgbase.subnum == -1) || (pMsgbase.subnum == 65535)) - descStr = "Electronic Mail"; - else - descStr = "Unspecified"; - } - return descStr; -} - -// Lets the sysop edit a user. -// -// Parameters: -// pUsername: The name of the user to edit -// -// Return value: A function containing the following properties: -// errorMsg: An error message on failure, or a blank string on success -function editUser(pUsername) -{ - var retObj = { - errorMsg: "" - }; - - if (typeof(pUsername) != "string") - { - retObj.errorMsg = "Given username is not a string"; - return retObj; - } - - // If the logged-in user is not a sysop, then just return. - if (!user.is_sysop) - { - retObj.errorMsg = "Only a sysop can edit a user"; - return retObj; - } - - // If the user exists, then let the sysop edit the user. - var userNum = system.matchuser(pUsername); - if (userNum != 0) - bbs.exec("*str_cmds uedit " + userNum); - else - retObj.errorMsg = "User \"" + pUsername + "\" not found"; - - return retObj; -} - -// Returns an object containing bare minimum properties necessary to -// display an invalid message header. Additionally, an object returned -// by this function will have an extra property, isBogus, that will be -// a boolean set to true. -// -// Parameters: -// pSubject: Optional - A string to use as the subject in the bogus message -// header object -function getBogusMsgHdr(pSubject) -{ - var msgHdr = { - subject: (typeof(pSubject) == "string" ? pSubject : ""), - when_imported_time: 0, - when_written_time: 0, - when_written_zone: 0, - date: "Fri, 1 Jan 1960 00:00:00 -0000", - attr: 0, - to: "Nobody", - from: "Nobody", - number: 0, - offset: 0, - isBogus: true - }; - return msgHdr; -} - -// Returns whether a message is readable to the user, based on its -// header and the sub-board code. This also checks pMsgHdrOrIdx for -// null; if it's null, this function will return false. -// -// Parameters: -// pMsgHdrOrIdx: The header or index object for the message -// pSubBoardCode: The internal code for the sub-board the message is in -// -// Return value: Boolean - Whether or not the message is readable for the user -function isReadableMsgHdr(pMsgHdrOrIdx, pSubBoardCode) -{ - if (pMsgHdrOrIdx === null) - return false; - // Let the sysop see unvalidated messages and private messages but not other users. - if (!user.is_sysop) - { - if (pSubBoardCode != "mail") - { - if ((msg_area.sub[pSubBoardCode].is_moderated && ((pMsgHdrOrIdx.attr & MSG_VALIDATED) == 0)) || - (((pMsgHdrOrIdx.attr & MSG_PRIVATE) == MSG_PRIVATE) && !userHandleAliasNameMatch(pMsgHdrOrIdx.to))) - { - return false; - } - } - } - // If the message is deleted, determine whether it should be viewable, based - // on the system settings. - if (((pMsgHdrOrIdx.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs()) - return false; - // The message voting and poll variables were added in sbbsdefs.js for - // Synchronet 3.17. Make sure they're defined before referencing them. - if (typeof(MSG_UPVOTE) != "undefined") - { - if ((pMsgHdrOrIdx.attr & MSG_UPVOTE) == MSG_UPVOTE) - return false; - } - if (typeof(MSG_DOWNVOTE) != "undefined") - { - if ((pMsgHdrOrIdx.attr & MSG_DOWNVOTE) == MSG_DOWNVOTE) - return false; - } - // Don't include polls as being unreadable messages - They just need to have - // their answer selections read from the header instead of the message body - /* - if (typeof(MSG_POLL) != "undefined") - { - if ((pMsgHdrOrIdx.attr & MSG_POLL) == MSG_POLL) - return false; - } - */ - return true; -} - -// Returns the header of the last readable message in a messagbase. If none is found, -// this will return null. -// -// Parameters: -// pMsgbase: An open MessageBase object -// pSubBoardCode: The internal code of the messagebase sub-board -// -// Return value: The header of the last readable message in the messagebase. If none is -// found, this will be null. -function getLastReadableMsgHdrInMsgbase(pMsgbase, pSubBoardCode) -{ - var hdrOfLastReadableMsg = null; - if (pMsgbase.is_open) - { - var numMsgs = pMsgbase.total_msgs; - for (var i = numMsgs - 1; i >= 0; --i) - { - if (isReadableMsgHdr(pMsgbase.get_msg_index(true, i, false), pSubBoardCode)) - { - hdrOfLastReadableMsg = pMsgbase.get_msg_header(true, i, false, false); - break; - } - } - } - return hdrOfLastReadableMsg; -} -// Returns the header of the last readable message in a messagbase (temporarily opens -// the sub-board). If none is found, this will return null. -// -// Parameters: -// pSubBoardCode: The internal code of the messagebase sub-board -// -// Return value: The header of the last readable message in the messagebase. If none is -// found, this will be null. -function getLastReadableMsgHdrInSubBoard(pSubBoardCode) -{ - var msgHdr = null; - var msgbase = new MsgBase(pSubBoardCode); - if (msgbase.open()) - { - msgHdr = getLastReadableMsgHdrInMsgbase(msgbase, pSubBoardCode); - msgbase.close(); - } - return msgHdr; -} - -// Returns the number of readable messages in a sub-board. -// -// Parameters: -// pMsgbase: The MsgBase object representing the sub-board -// pSubBoardCode: The internal code of the sub-board -// -// Return value: The number of readable messages in the sub-board -function numReadableMsgs(pMsgbase, pSubBoardCode) -{ - // The posts property in msg_area.sub[sub_code] and msg_area.grp_list.sub_list is the number - // of posts excluding vote posts - if (typeof(msg_area.sub[pSubBoardCode].posts) === "number") - return msg_area.sub[pSubBoardCode].posts; - else if ((pMsgbase !== null) && pMsgbase.is_open) - { - // Just return the total number of messages.. This isn't accurate, but it's fast. - return pMsgbase.total_msgs; - } - else if (pMsgbase === null) - { - var numMsgs = 0; - var msgBase = new MsgBase(pSubBoardCode); - if (msgBase.open()) - { - // Just return the total number of messages.. This isn't accurate, but it's fast. - numMsgs = msgBase.total_msgs; - msgBase.close(); - } - return numMsgs; - } - else - return 0; -} - -// Marks or unmarks vote messages as deleted (messages that have voting response data for a message with -// a given message number). -// -// Parameters: -// pMsgbase: A MessageBase object containing the messages to be deleted -// pMsgNum: The number of the message for which vote messages should be deleted -// pMsgID: The ID of the message for which vote messages should be deleted -// pDoDelete: Boolean: If true, then mark for deletion. If false, then remove the deleted attribute. -// pIsMailSub: Boolean - Whether or not it's the personal email area -// -// Return value: An object containing the following properties: -// numVoteMsgs: The number of vote messages for the given message number -// toggleVoteMsgsAffected: The number of vote messages that were deleted/undeleted -// allVoteMsgsAffected: Boolean - Whether or not all vote messages were deleted/undeleted -function toggleVoteMsgsDeleted(pMsgbase, pMsgNum, pMsgID, pDoDelete, pIsEmailSub) -{ - var retObj = { - numVoteMsgs: 0, - toggleVoteMsgsAffected: 0, - allVoteMsgsAffected: true - }; - - if ((pMsgbase === null) || !pMsgbase.is_open) - return retObj; - if (typeof(pMsgNum) != "number") - return retObj; - if (pIsEmailSub) - return retObj; - - // This relies on get_all_msg_headers() returning vote messages. - var msgHdrs = pMsgbase.get_all_msg_headers(true); - for (var msgHdrsProp in msgHdrs) - { - if (msgHdrs[msgHdrsProp] == null) - continue; - // If this header is a vote header and its thread_back or reply_id matches the given message, - // then we can delete this message. - if (isVoteHdr(msgHdrs[msgHdrsProp]) && (msgHdrs[msgHdrsProp].thread_back == pMsgNum) || (msgHdrs[msgHdrsProp].reply_id == pMsgID)) - { - ++retObj.numVoteMsgs; - var msgWasAffected = false; - if (pDoDelete) - msgWasAffected = pMsgbase.remove_msg(false, msgHdrs[msgHdrsProp].number); - else - { - var tmpMsgHdr = pMsgbase.get_msg_header(false, msgHdrs[msgHdrsProp].number, false); - if (tmpMsgHdr != null) - { - if ((tmpMsgHdr.attr & MSG_DELETE) == MSG_DELETE) - { - tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE; - msgWasAffected = pMsgbase.put_msg_header(false, msgHdrs[msgHdrsProp].number, tmpMsgHdr); - } - else - msgWasAffected = true; // No action needed - } - } - retObj.allVoteMsgsAffected = (retObj.allVoteMsgsAffected && msgWasAffected); - if (msgWasAffected) - ++retObj.toggleVoteMsgsAffected; - } - } - - return retObj; -} - -// Returns whether the user's scan_ptr for a sub-board is 4294967295 -// (0xffffffff, or ~0). That is a special value for the user's scan_ptr -// meaning it should point to the latest message in the messagebase. -// -// Parameters: -// pSubCode: The internal code of a sub-board -// -// Return value: Whether or not the user's scan_ptr for the sub-board is that special value -function subBoardScanPtrIsLatestMsgSpecialVal(pSubCode) -{ - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number") - return msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr); - else - return false; -} -// Returns whether a message number is 4294967295 (0xffffffff, or ~0). That is -// a special value for a message number meaning it should point to the latest -// message in the messagebase. -// -// Parameters: -// pMsgNum: A message number -// -// Return value: Whether or not the given message number is the special value -function msgNumIsLatestMsgSpecialVal(pMsgNum) -{ - return (pMsgNum == 0xffffffff); -} - -///////////////////////////////////////////////////////////////////////// -// Debug helper & error output functions - -// Prints information from a message header on the screen, for debugging purpurposes. -// -// Parameters: -// pMsgHdr: A message header object -function printMsgHdr(pMsgHdr) -{ - for (var prop in pMsgHdr) - { - if ((prop == "field_list") && (typeof(pMsgHdr[prop]) == "object")) - { - console.print(prop + ":\r\n"); - for (var objI = 0; objI < pMsgHdr[prop].length; ++objI) - { - console.print(" " + objI + ":\r\n"); - for (var innerProp in pMsgHdr[prop][objI]) - console.print(" " + innerProp + ": " + pMsgHdr[prop][objI][innerProp] + "\r\n"); - } - } - else - console.print(prop + ": " + pMsgHdr[prop] + "\r\n"); - } - console.pause(); -} - -// Closes a poll, using an existing MessageBase object. -// -// Parameters: -// pMsgbase: A MessageBase object representing the current sub-board. It -// must be open. -// pMsgNum: The message number (not the index) -// -// Return value: Boolean - Whether or not closing the poll succeeded -function closePollWithOpenMsgbase(pMsgbase, pMsgNum) -{ - var pollClosed = false; - if ((pMsgbase !== null) && pMsgbase.is_open) - { - var userNameOrAlias = user.alias; - // See if the poll was posted using the user's real name instead of - // their alias - var msgHdr = pMsgbase.get_msg_header(false, pMsgNum, false); - if ((msgHdr != null) && ((msgHdr.attr & MSG_POLL) == MSG_POLL)) - { - if (msgHdr.from.toUpperCase() == user.name.toUpperCase()) - userNameOrAlias = msgHdr.from; - } - // Close the poll (the close_poll() method was added in the Synchronet - // 3.17 build on August 19, 2017) - pollClosed = pMsgbase.close_poll(pMsgNum, userNameOrAlias); - } - return pollClosed; -} - -// Closes a poll. -// -// Parameters: -// pSubBoardCode: The internal code of the sub-board -// pMsgNum: The message number (not the index) -// -// Return value: Boolean - Whether or not closing the poll succeeded -function closePoll(pSubBoardCode, pMsgNum) -{ - var pollClosed = false; - var msgbase = new MsgBase(pSubBoardCode); - if (msgbase.open()) - { - pollClosed = closePollWithOpenMsgbase(msgbase, pMsgNum); - msgbase.close(); - } - return pollClosed; -} - -// Gets a message header from the messagebase, either by index (offset) or number. -// -// Parameters: -// pMsgbase: Optional messagebase object. If this is provided, then pSubBoardCode is not used. -// pSubBoardCode: The messagebase sub-board code -// pByIdx: Boolean - Whether or not to get the message header by index (if false, then by number) -// pMsgIdxOrNum: The message index or number of the message header to retrieve -// pExpandFields: Boolean - Whether or not to expand fields for the message header -function getHdrFromMsgbase(pMsgbase, pSubBoardCode, pByIdx, pMsgIdxOrNum, pExpandFields) -{ - var msgbaseIsOpen = false; - var msgbase = null; - var msgHdr = null; - if (pMsgbase == null) - { - msgbase = new MsgBase(pSubBoardCode); - msgbaseIsOpen = msgbase.open(); - } - else - { - msgbase = pMsgbase; - msgbaseIsOpen = pMsgbase.is_open; - } - if (msgbaseIsOpen) - { - var getMsgHdr = true; - if (pByIdx) - getMsgHdr = ((pMsgIdxOrNum >= 0) && (pMsgIdxOrNum < msgbase.total_msgs)) - if (getMsgHdr) - { - // TODO: I think we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - msgHdr = msgbase.get_msg_header(pByIdx, pMsgIdxOrNum, pExpandFields, true); // Last true: Include votes - } - if (pMsgbase == null) - msgbase.close(); - } - return msgHdr; -} - -// Inputs a string from the user, restricting their input to certain keys (optionally). -// -// Parameters: -// pKeys: A string containing valid characters for input. Optional -// pMaxNumChars: The maximum number of characters to input. Optional -// pCaseSensitive: Boolean - Whether or not the input should be case-sensitive. Optional. -// Defaults to true. If false, then the user input will be uppercased. -// -// Return value: A string containing the user's input -function consoleGetStrWithValidKeys(pKeys, pMaxNumChars, pCaseSensitive) -{ - var maxNumChars = 0; - if ((typeof(pMaxNumChars) == "number") && (pMaxNumChars > 0)) - maxNumChars = pMaxNumChars; - - var regexPattern = (typeof(pKeys) == "string" ? "[" + pKeys + "]" : "."); - var caseSensitive = (typeof(pCaseSensitive) == "boolean" ? pCaseSensitive : true); - var regex = new RegExp(regexPattern, (caseSensitive ? "" : "i")); - - var CTRL_H = "\x08"; - var BACKSPACE = CTRL_H; - var CTRL_M = "\x0d"; - var KEY_ENTER = CTRL_M; - - var modeBits = (caseSensitive ? K_NONE : K_UPPER); - var userInput = ""; - var continueOn = true; - while (continueOn) - { - var userChar = console.getkey(K_NOECHO|modeBits); - if (regex.test(userChar) && isPrintableChar(userChar)) - { - var appendChar = true; - if ((maxNumChars > 0) && (userInput.length >= maxNumChars)) - appendChar = false; - if (appendChar) - { - userInput += userChar; - if ((modeBits & K_NOECHO) == 0) - console.print(userChar); - } - } - else if (userChar == BACKSPACE) - { - if (userInput.length > 0) - { - if ((modeBits & K_NOECHO) == 0) - { - console.print(BACKSPACE); - console.print(" "); - console.print(BACKSPACE); - } - userInput = userInput.substr(0, userInput.length-1); - } - } - else if (userChar == KEY_ENTER) - { - continueOn = false; - if ((modeBits & K_NOCRLF) == 0) - console.crlf(); - } - } - return userInput; -} - -// Returns whether or not a character is printable. -// -// Parameters: -// pChar: A character to test -// -// Return value: Boolean - Whether or not the character is printable -function isPrintableChar(pChar) -{ - // Make sure pChar is valid and is a string. - if (typeof(pChar) != "string") - return false; - if (pChar.length == 0) - return false; - - // Make sure the character is a printable ASCII character in the range of 32 to 254, - // except for 127 (delete). - var charCode = pChar.charCodeAt(0); - return ((charCode > 31) && (charCode < 255) && (charCode != 127)); -} - -// Adds message attributes to a message header and saves it in the messagebase. -// To do that, this function first loads the messag header from the messagebase -// without expanded fields, applies the attributes, and then saves the header -// back to the messagebase. -// -// Parameters: -// pMsgbaseOrSubCode: An open MessageBase object or a sub-board code (string) -// pMsgNum: The number of the message to update -// pMsgAttrs: The message attributes to apply to the message (numeric bitfield) -// -// Return value: An object containing the following properties: -// saveSucceeded: Boolean - Whether or not the message header was successfully saved -// msgAttrs: A numeric bitfield containing the updated attributes of the message header -function applyAttrsInMsgHdrInMessagbase(pMsgbaseOrSubCode, pMsgNum, pMsgAttrs) -{ - var retObj = { - saveSucceeded: false, - msgAttrs: 0 - }; - - var msgbaseOpen = false; - var msgbase = null; - if (typeof(pMsgbaseOrSubCode) == "object") - { - msgbase = pMsgbaseOrSubCode; - msgbaseOpen = msgbase.is_open; - } - else if (typeof(pMsgbaseOrSubCode) == "string") - { - msgbase = new MsgBase(pMsgbaseOrSubCode); - msgbaseOpen = msgbase.open(); - } - else - return retObj; - - if (msgbaseOpen) - { - // Get the message header without expanded fields (we can't save it with - // expanded fields), then add the 'read' attribute and save it back to the messagebase. - var msgHdr = msgbase.get_msg_header(false, pMsgNum, false); - if (msgHdr != null) - { - msgHdr.attr |= pMsgAttrs; - // TODO: Occasional when going to next message area: - // Error: Error -110 adding SENDERNETADDR field to message header - retObj.saveSucceeded = msgbase.put_msg_header(false, pMsgNum, msgHdr); - if (retObj.saveSucceeded) - retObj.msgAttrs = msgHdr.attr; - else - { - writeToSysAndNodeLog("Failed to save message header with the following attributes: " + msgAttrsToString(pMsgAttrs), LOG_ERR); - writeToSysAndNodeLog(getMsgAreaDescStr(msgbase), LOG_ERR); - writeToSysAndNodeLog(format("Message offset: %d, number: %d", msgHdr.offset, msgHdr.number), LOG_ERR); - writeToSysAndNodeLog("Status: " + msgbase.status, LOG_ERR); - writeToSysAndNodeLog("Error: " + msgbase.error, LOG_ERR); - /* - // For sysops, output a debug message - if (user.is_sysop) - { - console.attributes = "N"; - console.crlf(); - console.print("* Failed to save msg header the with the following attributes: " + msgAttrsToString(pMsgAttrs)); - console.crlf(); - console.print("Status: " + msgbase.status); - console.crlf(); - console.print("Error: " + msgbase.error); - console.crlf(); - console.crlf(); - //console.print("put_msg_header params: false, " + msgHdr.number + ", header:\r\n"); - //console.print("put_msg_header params: true, " + msgHdr.offset + ", header:\r\n"); - //console.print("put_msg_header params: " + msgHdr.number + ", header:\r\n"); - printMsgHdr(msgHdr); - } - */ - } - } - - // If a sub-board code was passed in, then close the messagebase object - // that we created here. - if (typeof(pMsgbaseOrSubCode) == "string") - msgbase.close(); - } - - return retObj; -} - -// Converts a message attributes bitfield to a string. -// -// Parameters: -// pMsgAttrs: A numeric type with message attribute bits -// -// Return value: A string containing a list of the message attributes -function msgAttrsToString(pMsgAttrs) -{ - if (typeof(pMsgAttrs) != "number") - return ""; - - var attrsStr = ""; - if ((pMsgAttrs & MSG_PRIVATE) == MSG_PRIVATE) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_PRIVATE"; - } - if ((pMsgAttrs & MSG_READ) == MSG_READ) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_READ"; - } - if ((pMsgAttrs & MSG_PERMANENT) == MSG_PERMANENT) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_PERMANENT"; - } - if ((pMsgAttrs & MSG_LOCKED) == MSG_LOCKED) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_LOCKED"; - } - if ((pMsgAttrs & MSG_DELETE) == MSG_DELETE) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_DELETE"; - } - if ((pMsgAttrs & MSG_ANONYMOUS) == MSG_ANONYMOUS) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_ANONYMOUS"; - } - if ((pMsgAttrs & MSG_KILLREAD) == MSG_KILLREAD) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_KILLREAD"; - } - if ((pMsgAttrs & MSG_MODERATED) == MSG_MODERATED) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_MODERATED"; - } - if ((pMsgAttrs & MSG_VALIDATED) == MSG_VALIDATED) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_VALIDATED"; - } - if ((pMsgAttrs & MSG_REPLIED) == MSG_REPLIED) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_REPLIED"; - } - if ((pMsgAttrs & MSG_NOREPLY) == MSG_NOREPLY) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_NOREPLY"; - } - if ((pMsgAttrs & MSG_UPVOTE) == MSG_UPVOTE) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_UPVOTE"; - } - if ((pMsgAttrs & MSG_DOWNVOTE) == MSG_DOWNVOTE) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_DOWNVOTE"; - } - if ((pMsgAttrs & MSG_POLL) == MSG_POLL) - { - if (attrsStr.length > 0) - attrsStr += ", "; - attrsStr += "MSG_POLL"; - } - return attrsStr; -} - -// Returns the index of the first Synchronet attribute code before a given index -// in a string. -// -// Parameters: -// pStr: The string to search in -// pIdx: The index to search back from -// pSeriesOfAttrs: Optional boolean - Whether or not to look for a series of -// attributes. Defaults to false (look for just one attribute). -// pOnlyInWord: Optional boolean - Whether or not to look only in the current word -// (with words separated by whitespace). Defaults to false. -// -// Return value: The index of the first Synchronet attribute code before the given -// index in the string, or -1 if there is none or if the parameters -// are invalid -function strIdxOfSyncAttrBefore(pStr, pIdx, pSeriesOfAttrs, pOnlyInWord) -{ - if (typeof(pStr) != "string") - return -1; - if (typeof(pIdx) != "number") - return -1; - if ((pIdx < 0) || (pIdx >= pStr.length)) - return -1; - - var seriesOfAttrs = (typeof(pSeriesOfAttrs) == "boolean" ? pSeriesOfAttrs : false); - var onlyInWord = (typeof(pOnlyInWord) == "boolean" ? pOnlyInWord : false); - - var attrCodeIdx = pStr.lastIndexOf("\x01", pIdx-1); - if (attrCodeIdx > -1) - { - // If we are to only check the current word, then continue only if - // there isn't a space between the attribute code and the given index. - if (onlyInWord) - { - if (pStr.lastIndexOf(" ", pIdx-1) >= attrCodeIdx) - attrCodeIdx = -1; - } - } - if (attrCodeIdx > -1) - { - var syncAttrRegexWholeWord = /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i; - if (syncAttrRegexWholeWord.test(pStr.substr(attrCodeIdx, 2))) - { - if (seriesOfAttrs) - { - for (var i = attrCodeIdx - 2; i >= 0; i -= 2) - { - if (syncAttrRegexWholeWord.test(pStr.substr(i, 2))) - attrCodeIdx = i; - else - break; - } - } - } - else - attrCodeIdx = -1; - } - return attrCodeIdx; -} - -// Returns a string with any Synchronet color/attribute codes found in a string -// before a given index. -// -// Parameters: -// pStr: The string to search in -// pIdx: The index in the string to search before -// -// Return value: A string containing any Synchronet attribute codes found before -// the given index in the given string -function getAttrsBeforeStrIdx(pStr, pIdx) -{ - if (typeof(pStr) != "string") - return ""; - if (typeof(pIdx) != "number") - return ""; - if (pIdx < 0) - return ""; - - var idx = (pIdx < pStr.length ? pIdx : pStr.length-1); - var attrStartIdx = strIdxOfSyncAttrBefore(pStr, idx, true, false); - var attrEndIdx = strIdxOfSyncAttrBefore(pStr, idx, false, false); // Start of 2-character code - var attrsStr = ""; - if ((attrStartIdx > -1) && (attrEndIdx > -1)) - attrsStr = pStr.substring(attrStartIdx, attrEndIdx+2); - return attrsStr; -} - -// Given a message header, this function gets/calculates the message's -// upvotes, downvotes, and vote score, if that information is present. -// -// Parameters: -// pMsgHdr: A message header object -// -// Return value: An object containign the following properties: -// foundVoteInfo: Boolean - Whether the vote information exited in the header -// upvotes: The number of upvotes -// downvotes: The number of downvotes -// voteScore: The overall vote score -function getMsgUpDownvotesAndScore(pMsgHdr, pVerbose) -{ - var retObj = { - foundVoteInfo: false, - upvotes: 0, - downvotes: 0, - voteScore: 0 - }; - - if ((pMsgHdr.hasOwnProperty("total_votes") || pMsgHdr.hasOwnProperty("downvotes")) && pMsgHdr.hasOwnProperty("upvotes")) - { - retObj.foundVoteInfo = true; - retObj.upvotes = pMsgHdr.upvotes; - if (pMsgHdr.hasOwnProperty("downvotes")) - retObj.downvotes = pMsgHdr.downvotes; - else - retObj.downvotes = pMsgHdr.total_votes - pMsgHdr.upvotes; - retObj.voteScore = pMsgHdr.upvotes - retObj.downvotes; - if (pVerbose && user.is_sysop) - { - console.print("\x01n\r\n"); - console.print("Vote information from header:\r\n"); - console.print("Upvotes: " + pMsgHdr.upvotes + "\r\n"); - console.print("Downvotes: " + retObj.downvotes + "\r\n"); - console.print("Score: " + retObj.voteScore + "\r\n"); - console.pause(); - } - } - else - { - if (pVerbose && user.is_sysop) - console.print("\x01n\r\nMsg header does NOT have needed vote info\r\n\x01p"); - } - - return retObj; -} - -// Removes any initial Synchronet attribute(s) from a message body, -// which can sometimes color the whole message. -// -// Parameters: -// pMsgBody: The original message body -// -// Return value: The message body, with any initial color removed -function removeInitialColorFromMsgBody(pMsgBody) -{ - if (pMsgBody == null) - return ""; - - var msgBody = pMsgBody; - - var msgBodyLines = pMsgBody.split("\r\n", 3); - if (msgBodyLines.length == 3) - { - // A regex for attribute settings - Note that this doesn't contain some of the control codes - var onlySyncAttrsRegexWholeWord = new RegExp("^[\x01krgybmcw01234567hinq,;\.dtlasz]+$", 'i');; - var line1Match = /^ Re: .*/.test(strip_ctrl(msgBodyLines[0])); - var line2Match = /^ By: .* on .*/.test(strip_ctrl(msgBodyLines[1])); - var line3OnlySyncAttrs = onlySyncAttrsRegexWholeWord.test(msgBodyLines[2]); - if (line1Match && line2Match) - { - msgBodyLines = pMsgBody.split("\r\n"); - msgBodyLines[0] = strip_ctrl(msgBodyLines[0]); - msgBodyLines[1] = strip_ctrl(msgBodyLines[1]); - if (line3OnlySyncAttrs) - { - var originalLine3SyncAttrs = msgBodyLines[2]; - msgBodyLines[2] = strip_ctrl(msgBodyLines[2]); - // If the first word of the 4th line is only Synchronet attribute codes, - // and they're the same as the codes on the 3rd line, then remove them. - if (msgBodyLines.length >= 4) - { - var line4Words = msgBodyLines[3].split(" "); - if ((line4Words.length > 0) && onlySyncAttrsRegexWholeWord.test(line4Words[0]) && (line4Words[0] == originalLine3SyncAttrs)) - msgBodyLines[3] = msgBodyLines[3].substr(line4Words[0].length); - } - } - msgBody = ""; - for (var i = 0; i < msgBodyLines.length; ++i) - msgBody += msgBodyLines[i] + "\r\n"; - // Remove the trailing \r\n characters from msgBody - msgBody = msgBody.substr(0, msgBody.length-2); - } - } - - return msgBody; -} - -// Finds a user with a name, alias, or handle matching a given string. -// If system.matchuser() can't find it, this will iterate through all users -// to find the first user with a name, alias, or handle matching the given -// name. -function findUserNumWithName(pName) -{ - if (typeof(pName) !== "string" || pName.length == 0) - return 0; - - var userNum = system.matchuser(pName); - if (userNum == 0) - { - try - { - userNum = system.matchuserdata(U_NAME, pName); - } - catch (error) {} - } - if (userNum == 0) - { - try - { - userNum = system.matchuserdata(U_ALIAS, pName); - } - catch (error) {} - } - if (userNum == 0) - { - try - { - userNum = system.matchuserdata(U_HANDLE, pName); - } - catch (error) {} - } - return userNum; -} - -// Inputs a string from the user, with a timeout -// -// Parameters: -// pMode: The mode bits to use for the input (i.e., defined in sbbsdefs.js) -// pMaxLength: The maximum length of the string (0 or less for no limit) -// pTimeout: The timeout (in milliseconds). When the timeout is reached, -// input stops and the user's input is returned. -// -// Return value: The user's input (string) -function getStrWithTimeout(pMode, pMaxLength, pTimeout) -{ - var inputStr = ""; - - var mode = K_NONE; - if (typeof(pMode) == "number") - mode = pMode; - var maxWidth = 0; - if (typeof(pMaxLength) == "number") - maxWidth = pMaxLength; - var timeout = 0; - if (typeof(pTimeout) == "number") - timeout = pTimeout; - - var setNormalAttrAtEnd = false; - if (((mode & K_LINE) == K_LINE) && (maxWidth > 0) && console.term_supports(USER_ANSI)) - { - var curPos = console.getxy(); - printf("\x01n\x01w\x01h\x01" + "4%" + maxWidth + "s", ""); - console.gotoxy(curPos); - setNormalAttrAtEnd = true; - } - - var curPos = console.getxy(); - var userKey = ""; - do - { - userKey = console.inkey(mode, timeout); - if ((userKey.length > 0) && isPrintableChar(userKey)) - { - var allowAppendChar = true; - if ((maxWidth > 0) && (inputStr.length >= maxWidth)) - allowAppendChar = false; - if (allowAppendChar) - { - inputStr += userKey; - console.print(userKey); - ++curPos.x; - } - } - else if (userKey == BACKSPACE) - { - if (inputStr.length > 0) - { - inputStr = inputStr.substr(0, inputStr.length-1); - console.gotoxy(curPos.x-1, curPos.y); - console.print(" "); - console.gotoxy(curPos.x-1, curPos.y); - --curPos.x; - } - } - else if (userKey == KEY_ENTER) - userKey = ""; - } while(userKey.length > 0); - - if (setNormalAttrAtEnd) - console.attributes = "N"; - - return inputStr; -} - -// Calculates the page number (1-based) and top index for the page (0-based), -// given an item index. -// -// Parameters: -// pItemIdx: The index of the item -// pNumItemsPerPage: The number of items per page -// -// Return value: An object containing the following properties: -// pageNum: The page number of the item (1-based; will be 0 if not found) -// pageTopIdx: The index of the top item on the page (or -1 if not found) -function calcPageNumAndTopPageIdx(pItemIdx, pNumItemsPerPage) -{ - var retObj = { - pageNum: 0, - pageTopIdx: -1 - }; - - var pageNum = 1; - var topIdx = 0; - var continueOn = true; - do - { - var endIdx = topIdx + pNumItemsPerPage; - if ((pItemIdx >= topIdx) && (pItemIdx < endIdx)) - { - continueOn = false; - retObj.pageNum = pageNum; - retObj.pageTopIdx = topIdx; - } - else - { - ++pageNum; - topIdx = endIdx; - } - } while (continueOn); - - return retObj; -} - -// Finds the page number of a message group or sub-board, given some text to -// search for. -// -// Parameters: -// pText: The text to search for in the items -// pNumItemsPerPage: The number of items per page -// pSubBoard: Boolean - If true, search the sub-board list for the given group index. -// If false, search the group list. -// pStartItemIdx: The item index to start at -// pGrpIdx: The index of the group to search in (only for doing a sub-board search) -// -// Return value: An object containing the following properties: -// pageNum: The page number of the item (1-based; will be 0 if not found) -// pageTopIdx: The index of the top item on the page (or -1 if not found) -// itemIdx: The index of the item (or -1 if not found) -function getMsgAreaPageNumFromSearch(pText, pNumItemsPerPage, pSubBoard, pStartItemIdx, pGrpIdx) -{ - var retObj = { - pageNum: 0, - pageTopIdx: -1, - itemIdx: -1 - }; - - // Sanity checking - if ((typeof(pText) != "string") || (typeof(pNumItemsPerPage) != "number") || (typeof(pSubBoard) != "boolean")) - return retObj; - - // Convert the text to uppercase for case-insensitive searching - var srchText = pText.toUpperCase(); - if (pSubBoard) - { - if ((typeof(pGrpIdx) == "number") && (pGrpIdx >= 0) && (pGrpIdx < msg_area.grp_list.length)) - { - // Go through the sub-board list of the given group and - // search for text in the descriptions - for (var i = pStartItemIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i) - { - if ((msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(srchText) > -1) || - (msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(srchText) > -1)) - { - retObj.itemIdx = i; - // Figure out the page number and top index for the page - var pageObj = calcPageNumAndTopPageIdx(i, pNumItemsPerPage); - if ((pageObj.pageNum > 0) && (pageObj.pageTopIdx > -1)) - { - retObj.pageNum = pageObj.pageNum; - retObj.pageTopIdx = pageObj.pageTopIdx; - } - break; - } - } - } - } - else - { - // Go through the message group list and look for a match - for (var i = pStartItemIdx; i < msg_area.grp_list.length; ++i) - { - if ((msg_area.grp_list[i].name.toUpperCase().indexOf(srchText) > -1) || - (msg_area.grp_list[i].description.toUpperCase().indexOf(srchText) > -1)) - { - retObj.itemIdx = i; - // Figure out the page number and top index for the page - var pageObj = calcPageNumAndTopPageIdx(i, pNumItemsPerPage); - if ((pageObj.pageNum > 0) && (pageObj.pageTopIdx > -1)) - { - retObj.pageNum = pageObj.pageNum; - retObj.pageTopIdx = pageObj.pageTopIdx; - } - break; - } - } - } - - return retObj; -} - -// Finds a message group index with search text, matching either the name or -// description, case-insensitive. -// -// Parameters: -// pSearchText: The name/description text to look for -// pStartItemIdx: The item index to start at. Defaults to 0 -// -// Return value: The index of the message group, or -1 if not found -function findMsgGrpIdxFromText(pSearchText, pStartItemIdx) -{ - if (typeof(pSearchText) != "string") - return -1; - - var grpIdx = -1; - - var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0); - if ((startIdx < 0) || (startIdx > msg_area.grp_list.length)) - startIdx = 0; - - // Go through the message group list and look for a match - var searchTextUpper = pSearchText.toUpperCase(); - for (var i = startIdx; i < msg_area.grp_list.length; ++i) - { - if ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (msg_area.grp_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - grpIdx = i; - break; - } - } - - return grpIdx; -} - -// Finds a message group index with search text, matching either the name or -// description, case-insensitive. -// -// Parameters: -// pGrpIdx: The index of the message group -// pSearchText: The name/description text to look for -// pStartItemIdx: The item index to start at. Defaults to 0 -// -// Return value: The index of the message group, or -1 if not found -function findSubBoardIdxFromText(pGrpIdx, pSearchText, pStartItemIdx) -{ - if (typeof(pGrpIdx) != "number") - return -1; - if (typeof(pSearchText) != "string") - return -1; - - var subBoardIdx = -1; - - var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0); - if ((startIdx < 0) || (startIdx > msg_area.grp_list[pGrpIdx].sub_list.length)) - startIdx = 0; - - // Go through the message group list and look for a match - var searchTextUpper = pSearchText.toUpperCase(); - for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i) - { - if ((msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) || - (msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1)) - { - subBoardIdx = i; - break; - } - } - - return subBoardIdx; -} - -// Searches for a @MSG_TO @-code in a string and inserts a color/attribute code -// before the @-code in the string. -// -// Parameters: -// pStr: The string to look in -// pToUserColor: The color/attribute code to insert before the @MSG_TO @-code -// -// Return value: A string with the given color/attribute code inserted before the -// @MSG_TO @-code -function strWithToUserColor(pStr, pToUserColor) -{ - if ((typeof(pStr) != "string") || (typeof(pToUserColor) != "string")) - return ""; - if (pToUserColor.length == 0) - return pStr; - - // Find start & end indexes of a @MSG_TO* @-code, i.e., - // @MSG_TO, @MSG_TO_NAME, @MSG_TO_EXT, @MSG_TO_NET - var toCodeStartIdx = pStr.indexOf("@MSG_TO"); - if (toCodeStartIdx < 0) - return pStr; - // Insert the color in the right position and return the line - return pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx) + "\x01n"; - /* - // Insert the color in the right position, and - // put a \x01n right after the end of the @-code - var str = ""; - var toCodeEndIdx = pStr.indexOf("@", toCodeStartIdx+1); - if (toCodeEndIdx >= 0) - { - str = pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx, toCodeEndIdx-toCodeStartIdx+1) - + "\x01n" + pStr.substr(toCodeEndIdx); - } - else - str = pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx) + "\x01n"; - return str; - */ -} - -// Gets the value of the user's current scan_ptr in a sub-board, or if it's -// the 'last message' special value, returns the message number of the last -// readable message in the sub-board (this is the message number, not the index). -// -// Parameters: -// pSubCode: A sub-board internal code -// -// Return value: The user's scan_ptr value or the message number of the -// last readable message in the sub-board -function GetScanPtrOrLastMsgNum(pSubCode) -{ - var msgNumToReturn = 0; - // If the user's scan_ptr for the sub-board isn't the 'last message' - // special value, then use it; otherwise, use the latest readable - // message number. - if (!subBoardScanPtrIsLatestMsgSpecialVal(pSubCode)) - msgNumToReturn = msg_area.sub[pSubCode].scan_ptr; - else - { - var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode); - if (lastReadableMsgHdr != null) - msgNumToReturn = lastReadableMsgHdr.number; - } - - return msgNumToReturn; -} - -// Returns whether a message header has one of the attachment flags -// enabled (for Synchtonet 3.17 or newer). -// -// Parameters: -// pMsgHdr: A message header (returned from MsgBase.get_msg_header()) -// -// Return value: Boolean - Whether or not the message has one of the attachment flags -function msgHdrHasAttachmentFlag(pMsgHdr) -{ - if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.auxattr) === "undefined") - return false; - - var attachmentFlag = false; - if (typeof(MSG_FILEATTACH) !== "undefined" && typeof(MSG_MIMEATTACH) !== "undefined") - attachmentFlag = (pMsgHdr.auxattr & (MSG_FILEATTACH|MSG_MIMEATTACH)) > 0; - return attachmentFlag; -} - -// Allows the user to download a message and its attachments, using the newer -// Synchronet interface (the function bbs.download_msg_attachments() must exist). -// -// Parameters: -// pMsgHdr: The message header -// pSubCode: The sub-board code that the message is in -function allowUserToDownloadMessage_NewInterface(pMsgHdr, pSubCode) -{ - if (typeof(bbs.download_msg_attachments) !== "function") - return; - if (typeof(pSubCode) !== "string") - return; - if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.number) == "undefined") - return; - - var msgBase = new MsgBase(pSubCode); - if (msgBase.open()) - { - // bbs.download_msg_attachments() requires a message header returned - // by MsgBase.get_msg_header() - var msgHdrForDownloading = msgBase.get_msg_header(false, pMsgHdr.number, false); - // Allow the user to download the message - if (!console.noyes("Download message", P_NOCRLF)) - { - if (!download_msg(msgHdrForDownloading, msgBase, console.yesno("Plain-text only"))) - console.print("\x01n\r\nFailed\r\n"); - } - // Allow the user to download the attachments - console.creturn(); - bbs.download_msg_attachments(msgHdrForDownloading); - msgBase.close(); - } -} - -// From msglist.js - Prompts the user if they want to download the message text -function download_msg(msg, msgbase, plain_text) -{ - var fname = system.temp_dir + "msg_" + msg.number + ".txt"; - var f = new File(fname); - if(!f.open("wb")) - return false; - var text = msgbase.get_msg_body(msg - ,/* strip ctrl-a */false - ,/* dot-stuffing */false - ,/* tails */true - ,plain_text); - f.write(msg.get_rfc822_header(/* force_update: */false, /* unfold: */false - ,/* default_content_type */!plain_text)); - f.writeln(text); - f.close(); - return bbs.send_file(fname); -} - - -////////// Message list sort functions - -// For sorting message headers by date & time -// -// Parameters: -// msgHdrA: The first message header -// msgHdrB: The second message header -// -// Return value: -1, 0, or 1, depending on whether header A comes before, -// is equal to, or comes after header B -function sortMessageHdrsByDateTime(msgHdrA, msgHdrB) -{ - // Return -1, 0, or 1, depending on whether msgHdrA's date & time comes - // before, is equal to, or comes after msgHdrB's date & time - // Convert when_written_time to local time before comparing the times - var localWrittenTimeA = msgWrittenTimeToLocalBBSTime(msgHdrA); - var localWrittenTimeB = msgWrittenTimeToLocalBBSTime(msgHdrB); - var yearA = +strftime("%Y", localWrittenTimeA); - var monthA = +strftime("%m", localWrittenTimeA); - var dayA = +strftime("%d", localWrittenTimeA); - var hourA = +strftime("%H", localWrittenTimeA); - var minuteA = +strftime("%M", localWrittenTimeA); - var secondA = +strftime("%S", localWrittenTimeA); - var yearB = +strftime("%Y", localWrittenTimeB); - var monthB = +strftime("%m", localWrittenTimeB); - var dayB = +strftime("%d", localWrittenTimeB); - var hourB = +strftime("%H", localWrittenTimeB); - var minuteB = +strftime("%M", localWrittenTimeB); - var secondB = +strftime("%S", localWrittenTimeB); - if (yearA < yearB) - return -1; - else if (yearA > yearB) - return 1; - else - { - if (monthA < monthB) - return -1; - else if (monthA > monthB) - return 1; - else - { - if (dayA < dayB) - return -1; - else if (dayA > dayB) - return 1; - else - { - if (hourA < hourB) - return -1; - else if (hourA > hourB) - return 1; - else - { - if (minuteA < minuteB) - return -1; - else if (minuteA > minuteB) - return 1; - else - { - if (secondA < secondB) - return -1; - else if (secondA > secondB) - return 1; - else - return 0; - } - } - } - } - } -} - -// Returns an array of internal sub-board codes to scan for a given scan scope. -// -// Parameters: -// pScanScopeChar: A string specifying "A" for all sub-boards, "G" for current -// message group sub-boards, or "S" for the current sub-board -// -// Return value: An array of internal sub-board codes for sub-boards to scan -function getSubBoardsToScanArray(pScanScopeChar) -{ - var subBoardsToScan = []; - if (pScanScopeChar == "A") // All sub-board scan - { - for (var grpIndex = 0; grpIndex < msg_area.grp_list.length; ++grpIndex) - { - for (var subIndex = 0; subIndex < msg_area.grp_list[grpIndex].sub_list.length; ++subIndex) - subBoardsToScan.push(msg_area.grp_list[grpIndex].sub_list[subIndex].code); - } - } - else if (pScanScopeChar == "G") // Group scan - { - for (var subIndex = 0; subIndex < msg_area.grp_list[bbs.curgrp].sub_list.length; ++subIndex) - subBoardsToScan.push(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); - } - else if (pScanScopeChar == "S") // Current sub-board scan - subBoardsToScan.push(bbs.cursub_code); - return subBoardsToScan; -} - -// Returns whether a user number is valid (only an actual, active user) -// -// Parameters: -// pUserNum: A user number -// -// Return value: Boolean - Whether or not the given user number is valid -function isValidUserNum(pUserNum) -{ - if (typeof(pUserNum) !== "number") - return false; - if (pUserNum < 1 || pUserNum > system.lastuser) - return false; - - var userIsValid = false; - var theUser = new User(pUserNum); - if (theUser != null && (theUser.settings & USER_DELETED) == 0 && (theUser.settings & USER_INACTIVE) == 0) - userIsValid = true; - return userIsValid; -} - -// Returns the index of the last ANSI code in a string. -// -// Parameters: -// pStr: The string to search in -// pANSIRegexes: An array of regular expressions to use for searching for ANSI codes -// -// Return value: The index of the last ANSI code in the string, or -1 if not found -function idxOfLastANSICode(pStr, pANSIRegexes) -{ - var lastANSIIdx = -1; - for (var i = 0; i < pANSIRegexes.length; ++i) - { - var lastANSIIdxTmp = regexLastIndexOf(pStr, pANSIRegexes[i]); - if (lastANSIIdxTmp > lastANSIIdx) - lastANSIIdx = lastANSIIdxTmp; - } - return lastANSIIdx; -} - -// Returns the index of the first ANSI code in a string. -// -// Parameters: -// pStr: The string to search in -// pANSIRegexes: An array of regular expressions to use for searching for ANSI codes -// -// Return value: The index of the first ANSI code in the string, or -1 if not found -function idxOfFirstANSICode(pStr, pANSIRegexes) -{ - var firstANSIIdx = -1; - for (var i = 0; i < pANSIRegexes.length; ++i) - { - var firstANSIIdxTmp = regexFirstIndexOf(pStr, pANSIRegexes[i]); - if (firstANSIIdxTmp > firstANSIIdx) - firstANSIIdx = firstANSIIdxTmp; - } - return firstANSIIdx; -} - -// Returns the number of times an ANSI code is matched in a string. -// -// Parameters: -// pStr: The string to search in -// pANSIRegexes: An array of regular expressions to use for searching for ANSI codes -// -// Return value: The number of ANSI code matches in the string -function countANSICodes(pStr, pANSIRegexes) -{ - var ANSICount = 0; - for (var i = 0; i < pANSIRegexes.length; ++i) - { - var matches = pStr.match(pANSIRegexes[i]); - if (matches != null) - ANSICount += matches.length; - } - return ANSICount; -} - -// Removes ANSI codes from a string. -// -// Parameters: -// pStr: The string to remove ANSI codes from -// pANSIRegexes: An array of regular expressions to use for searching for ANSI codes -// -// Return value: A version of the string without ANSI codes -function removeANSIFromStr(pStr, pANSIRegexes) -{ - if (typeof(pStr) != "string") - return ""; - - var theStr = pStr; - for (var i = 0; i < pANSIRegexes.length; ++i) - theStr = theStr.replace(pANSIRegexes[i], ""); - return theStr; -} - -// Returns the last index in a string where a regex is found. -// From this page: -// http://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr -// -// Parameters: -// pStr: The string to search -// pRegex: The regular expression to match in the string -// pStartPos: Optional - The starting position in the string. If this is not -// passed, then the end of the string will be used. -// -// Return value: The last index in the string where the regex is found, or -1 if not found. -function regexLastIndexOf(pStr, pRegex, pStartPos) -{ - pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : "")); - if (typeof(pStartPos) == "undefined") - pStartPos = pStr.length; - else if (pStartPos < 0) - pStartPos = 0; - var stringToWorkWith = pStr.substring(0, pStartPos + 1); - var lastIndexOf = -1; - var nextStop = 0; - while ((result = pRegex.exec(stringToWorkWith)) != null) - { - lastIndexOf = result.index; - pRegex.lastIndex = ++nextStop; - } - return lastIndexOf; -} - -// Returns the first index in a string where a regex is found. -// -// Parameters: -// pStr: The string to search -// pRegex: The regular expression to match in the string -// -// Return value: The first index in the string where the regex is found, or -1 if not found. -function regexFirstIndexOf(pStr, pRegex) -{ - pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : "")); - var indexOfRegex = -1; - var nextStop = 0; - while ((result = pRegex.exec(pStr)) != null) - { - indexOfRegex = result.index; - pRegex.lastIndex = ++nextStop; - } - return indexOfRegex; -} - -// Returns whether or not a string has any ASCII drawing characters (typically above ASCII value 127). -// -// Parameters: -// pText: The text to check -// -// Return value: Boolean - Whether or not the text has any ASCII drawing characters -function textHasDrawingChars(pText) -{ - if (typeof(pText) !== "string" || pText.length == 0) - return false; - - if (typeof(textHasDrawingChars.chars) === "undefined") - { - textHasDrawingChars.chars = [ascii(169), ascii(170)]; - for (var asciiVal = 174; asciiVal <= 223; ++asciiVal) - textHasDrawingChars.chars.push(ascii(asciiVal)); - textHasDrawingChars.chars.push(ascii(254)); - } - var drawingCharsFound = false; - for (var i = 0; i < textHasDrawingChars.chars.length && !drawingCharsFound; ++i) - drawingCharsFound = drawingCharsFound || (pText.indexOf(textHasDrawingChars.chars[i]) > -1); - return drawingCharsFound; -} - -// Returns a string with a character repeated a given number of times -// -// Parameters: -// pChar: The character to repeat in the string -// pNumTimes: The number of times to repeat the character -// -// Return value: A string with the given character repeated the given number of times -function charStr(pChar, pNumTimes) -{ - if (typeof(pChar) !== "string" || pChar.length == 0 || typeof(pNumTimes) !== "number" || pNumTimes < 1) - return ""; - - var str = ""; - for (var i = 0; i < pNumTimes; ++i) - str += pChar; - return str; -} - -// Returns whether the logged-in user can view deleted messages. -function canViewDeletedMsgs() -{ - var usersVDM = ((system.settings & SYS_USRVDELM) == SYS_USRVDELM); - var sysopVDM = ((system.settings & SYS_SYSVDELM) == SYS_SYSVDELM); - return (usersVDM || (user.is_sysop && sysopVDM)); -} - -// Returns whether or not a message header is a vote header -// -// Parameters: -// pMsgHdrOrIndex: A message header or index object -// -// Return value: Boolean - Whether or not the header is a vote header -function isVoteHdr(pMsgHdrOrIndex) -{ - if (typeof(pMsgHdrOrIndex) !== "object" || !pMsgHdrOrIndex.hasOwnProperty("attr")) - return false; - return (((pMsgHdrOrIndex.attr & MSG_VOTE) == MSG_VOTE) || ((pMsgHdrOrIndex.attr & MSG_UPVOTE) == MSG_UPVOTE) || ((pMsgHdrOrIndex.attr & MSG_DOWNVOTE) == MSG_DOWNVOTE)); -} - -// Updates scan_ptr and/or last_read for a sub-board -// -// Parameters: -// pSubCode: The internal code for the sub-board being read -// pMsgHdr: A message header or index object (the message number will be used) -// pDoingMsgScan: Boolean - Whether or not a scan is being done -function updateScanPtrAndOrLastRead(pSubCode, pMsgHdr, pDoingMsgScan) -{ - // If not reading personal email, then update the scan & last read message pointers. - if (pSubCode != "mail") - { - if (pDoingMsgScan) - { - if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number") - { - if (!msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr) && msg_area.sub[pSubCode].scan_ptr < pMsgHdr.number) - msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number; - } - else - msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number; - //if (pMsgHdr.number > GetScanPtrOrLastMsgNum(pSubCode)) - // msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number; - - if (pMsgHdr.number > msg_area.sub[pSubCode].last_read) - msg_area.sub[pSubCode].last_read = pMsgHdr.number; - } - else - { - msg_area.sub[pSubCode].last_read = pMsgHdr.number; - //if (pMsgHdr.number > msg_area.sub[pSubCode].last_read) - // msg_area.sub[pSubCode].last_read = pMsgHdr.number; - } - } -} - - -/////////////////////////////////////////////////////////////////////////////////// -// ChoiceScrollbox stuff (this was copied from SlyEdit_Misc.js; maybe there's a better way to do this) - -// Returns the minimum width for a ChoiceScrollbox -function ChoiceScrollbox_MinWidth() -{ - return 73; // To leave room for the navigation text in the bottom border -} - -// ChoiceScrollbox constructor -// -// Parameters: -// pLeftX: The horizontal component (column) of the upper-left coordinate -// pTopY: The vertical component (row) of the upper-left coordinate -// pWidth: The width of the box (including the borders) -// pHeight: The height of the box (including the borders) -// pTopBorderText: The text to include in the top border -// pCfgObj: The script/program configuration object (color settings are used) -// pAddTCharsAroundTopText: Optional, boolean - Whether or not to use left & right T characters -// around the top border text. Defaults to true. -// pReplaceTopTextSpacesWithBorderChars: Optional, boolean - Whether or not to replace -// spaces in the top border text with border characters. -// Defaults to false. -function ChoiceScrollbox(pLeftX, pTopY, pWidth, pHeight, pTopBorderText, pCfgObj, - pAddTCharsAroundTopText, pReplaceTopTextSpacesWithBorderChars) -{ - if (pCfgObj == null || typeof(pCfgObj) !== "object") - pCfgObj = {}; - if (pCfgObj.colors == null || typeof(pCfgObj.colors) !== "object") - { - pCfgObj.colors = { - listBoxBorder: "\x01n\x01g", - listBoxBorderText: "\x01n\x01b\x01h", - listBoxItemText: "\x01n\x01c", - listBoxItemHighlight: "\x01n\x01" + "4\x01w\x01h" - }; - } - else - { - if (!pCfgObj.colors.hasOwnProperty("listBoxBorder")) - pCfgObj.colors.listBoxBorder = "\x01n\x01g"; - if (!pCfgObj.colors.hasOwnProperty("listBoxBorderText")) - pCfgObj.colors.listBoxBorderText = "\x01n\x01b\x01h"; - if (!pCfgObj.colors.hasOwnProperty("listBoxItemText")) - pCfgObj.colors.listBoxItemText = "\x01n\x01c"; - if (!pCfgObj.colors.hasOwnProperty("listBoxItemHighlight")) - pCfgObj.colors.listBoxItemHighlight = "\x01n\x01" + "4\x01w\x01h"; - } - - // The default is to add left & right T characters around the top border - // text. But also use pAddTCharsAroundTopText if it's a boolean. - var addTopTCharsAroundText = true; - if (typeof(pAddTCharsAroundTopText) == "boolean") - addTopTCharsAroundText = pAddTCharsAroundTopText; - // If pReplaceTopTextSpacesWithBorderChars is true, then replace the spaces - // in pTopBorderText with border characters. - if (pReplaceTopTextSpacesWithBorderChars) - { - var startIdx = 0; - var firstSpcIdx = pTopBorderText.indexOf(" ", 0); - // Look for the first non-space after firstSpaceIdx - var nonSpcIdx = -1; - for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i) - { - if (pTopBorderText.charAt(i) != " ") - nonSpcIdx = i; - } - var firstStrPart = ""; - var lastStrPart = ""; - var numSpaces = 0; - while ((firstSpcIdx > -1) && (nonSpcIdx > -1)) - { - firstStrPart = pTopBorderText.substr(startIdx, (firstSpcIdx-startIdx)); - lastStrPart = pTopBorderText.substr(nonSpcIdx); - numSpaces = nonSpcIdx - firstSpcIdx; - if (numSpaces > 0) - { - pTopBorderText = firstStrPart + "\x01n" + pCfgObj.colors.listBoxBorder; - for (var i = 0; i < numSpaces; ++i) - pTopBorderText += HORIZONTAL_SINGLE; - pTopBorderText += "\x01n" + pCfgObj.colors.listBoxBorderText + lastStrPart; - } - - // Look for the next space and non-space character after that. - firstSpcIdx = pTopBorderText.indexOf(" ", nonSpcIdx); - // Look for the first non-space after firstSpaceIdx - nonSpcIdx = -1; - for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i) - { - if (pTopBorderText.charAt(i) != " ") - nonSpcIdx = i; - } - } - } - - this.programCfgObj = pCfgObj; - - var minWidth = ChoiceScrollbox_MinWidth(); - - this.dimensions = { - topLeftX: pLeftX, - topLeftY: pTopY, - width: 0, - height: pHeight, - bottomRightX: 0, - bottomRightY: 0 - }; - // Make sure the width is the minimum width - if ((pWidth < 0) || (pWidth < minWidth)) - this.dimensions.width = minWidth; - else - this.dimensions.width = pWidth; - this.dimensions.bottomRightX = this.dimensions.topLeftX + this.dimensions.width - 1; - this.dimensions.bottomRightY = this.dimensions.topLeftY + this.dimensions.height - 1; - - // The text item array and member variables relating to it and the items - // displayed on the screen during the input loop - this.txtItemList = []; - this.chosenTextItemIndex = -1; - this.topItemIndex = 0; - this.bottomItemIndex = 0; - - // Top border string - var innerBorderWidth = this.dimensions.width - 2; - // Calculate the maximum top border text length to account for the left/right - // T chars and "Page #### of ####" text - var maxTopBorderTextLen = innerBorderWidth - (pAddTCharsAroundTopText ? 21 : 19); - if (strip_ctrl(pTopBorderText).length > maxTopBorderTextLen) - pTopBorderText = pTopBorderText.substr(0, maxTopBorderTextLen); - this.topBorder = "\x01n" + pCfgObj.colors.listBoxBorder + UPPER_LEFT_SINGLE; - if (addTopTCharsAroundText) - this.topBorder += RIGHT_T_SINGLE; - this.topBorder += "\x01n" + pCfgObj.colors.listBoxBorderText - + pTopBorderText + "\x01n" + pCfgObj.colors.listBoxBorder; - if (addTopTCharsAroundText) - this.topBorder += LEFT_T_SINGLE; - const topBorderTextLen = strip_ctrl(pTopBorderText).length; - var numHorizBorderChars = innerBorderWidth - topBorderTextLen - 20; - if (addTopTCharsAroundText) - numHorizBorderChars -= 2; - for (var i = 0; i <= numHorizBorderChars; ++i) - this.topBorder += HORIZONTAL_SINGLE; - this.topBorder += RIGHT_T_SINGLE + "\x01n" + pCfgObj.colors.listBoxBorderText - + "Page 1 of 1" + "\x01n" + pCfgObj.colors.listBoxBorder + LEFT_T_SINGLE - + UPPER_RIGHT_SINGLE; - - // Bottom border string - this.btmBorderNavText = "\x01n\x01h\x01c" + UP_ARROW + "\x01b, \x01c" + DOWN_ARROW + "\x01b, \x01cN\x01y)\x01bext, \x01cP\x01y)\x01brev, " - + "\x01cF\x01y)\x01birst, \x01cL\x01y)\x01bast, \x01cHOME\x01b, \x01cEND\x01b, \x01cEnter\x01y=\x01bSelect, " - + "\x01cESC\x01n\x01c/\x01h\x01cQ\x01y=\x01bEnd"; - this.bottomBorder = "\x01n" + pCfgObj.colors.listBoxBorder + LOWER_LEFT_SINGLE - + RIGHT_T_SINGLE + this.btmBorderNavText + "\x01n" + pCfgObj.colors.listBoxBorder - + LEFT_T_SINGLE; - var numCharsRemaining = this.dimensions.width - strip_ctrl(this.btmBorderNavText).length - 6; - for (var i = 0; i < numCharsRemaining; ++i) - this.bottomBorder += HORIZONTAL_SINGLE; - this.bottomBorder += LOWER_RIGHT_SINGLE; - - // Item format strings - this.listIemFormatStr = "\x01n" + pCfgObj.colors.listBoxItemText + "%-" - + +(this.dimensions.width-2) + "s"; - this.listIemHighlightFormatStr = "\x01n" + pCfgObj.colors.listBoxItemHighlight + "%-" - + +(this.dimensions.width-2) + "s"; - - // Key functionality override function pointers - this.enterKeyOverrideFn = null; - - // inputLoopeExitKeys is an object containing additional keypresses that will - // exit the input loop. - this.inputLoopExitKeys = {}; - - // For drawing the menu - this.pageNum = 0; - this.numPages = 0; - this.numItemsPerPage = 0; - this.maxItemWidth = 0; - this.pageNumTxtStartX = 0; - - // Input loop quit override (to be used in overridden enter function if needed to quit the input loop there - this.continueInputLoopOverride = true; - - // Object functions - this.addTextItem = ChoiceScrollbox_AddTextItem; // Returns the index of the item - this.getTextItem = ChoiceScrollbox_GetTextIem; - this.replaceTextItem = ChoiceScrollbox_ReplaceTextItem; - this.delTextItem = ChoiceScrollbox_DelTextItem; - this.chgCharInTextItem = ChoiceScrollbox_ChgCharInTextItem; - this.getChosenTextItemIndex = ChoiceScrollbox_GetChosenTextItemIndex; - this.setItemArray = ChoiceScrollbox_SetItemArray; // Sets the item array; returns whether or not it was set. - this.clearItems = ChoiceScrollbox_ClearItems; // Empties the array of items - this.setEnterKeyOverrideFn = ChoiceScrollbox_SetEnterKeyOverrideFn; - this.clearEnterKeyOverrideFn = ChoiceScrollbox_ClearEnterKeyOverrideFn; - this.addInputLoopExitKey = ChoiceScrollbox_AddInputLoopExitKey; - this.setBottomBorderText = ChoiceScrollbox_SetBottomBorderText; - this.drawBorder = ChoiceScrollbox_DrawBorder; - this.drawInnerMenu = ChoiceScrollbox_DrawInnerMenu; - this.refreshOnScreen = ChoiceScrollbox_RefreshOnScreen; - this.refreshItemCharOnScreen = ChoiceScrollbox_RefreshItemCharOnScreen; - // Does the input loop. Returns an object with the following properties: - // itemWasSelected: Boolean - Whether or not an item was selected - // selectedIndex: The index of the selected item - // selectedItem: The text of the selected item - // lastKeypress: The last key pressed by the user - this.doInputLoop = ChoiceScrollbox_DoInputLoop; -} -function ChoiceScrollbox_AddTextItem(pTextLine, pStripCtrl) -{ - var stripCtrl = true; - if (typeof(pStripCtrl) == "boolean") - stripCtrl = pStripCtrl; - - if (stripCtrl) - this.txtItemList.push(strip_ctrl(pTextLine)); - else - this.txtItemList.push(pTextLine); - // Return the index of the added item - return this.txtItemList.length-1; -} -function ChoiceScrollbox_GetTextIem(pItemIndex) -{ - if (typeof(pItemIndex) != "number") - return ""; - if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length)) - return ""; - - return this.txtItemList[pItemIndex]; -} -function ChoiceScrollbox_ReplaceTextItem(pItemIndexOrStr, pNewItem) -{ - if (typeof(pNewItem) != "string") - return false; - - // Find the item index - var itemIndex = -1; - if (typeof(pItemIndexOrStr) == "number") - { - if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length)) - return false; - else - itemIndex = pItemIndexOrStr; - } - else if (typeof(pItemIndexOrStr) == "string") - { - itemIndex = -1; - for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i) - { - if (this.txtItemList[i] == pItemIndexOrStr) - itemIndex = i; - } - } - else - return false; - - // Replace the item - var replacedIt = false; - if ((itemIndex > -1) && (itemIndex < this.txtItemList.length)) - { - this.txtItemList[itemIndex] = pNewItem; - replacedIt = true; - } - return replacedIt; -} -function ChoiceScrollbox_DelTextItem(pItemIndexOrStr) -{ - // Find the item index - var itemIndex = -1; - if (typeof(pItemIndexOrStr) == "number") - { - if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length)) - return false; - else - itemIndex = pItemIndexOrStr; - } - else if (typeof(pItemIndexOrStr) == "string") - { - itemIndex = -1; - for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i) - { - if (this.txtItemList[i] == pItemIndexOrStr) - itemIndex = i; - } - } - else - return false; - - // Remove the item - var removedIt = false; - if ((itemIndex > -1) && (itemIndex < this.txtItemList.length)) - { - this.txtItemList = this.txtItemList.splice(itemIndex, 1); - removedIt = true; - } - return removedIt; -} -function ChoiceScrollbox_ChgCharInTextItem(pItemIndexOrStr, pStrIndex, pNewText) -{ - // Find the item index - var itemIndex = -1; - if (typeof(pItemIndexOrStr) == "number") - { - if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length)) - return false; - else - itemIndex = pItemIndexOrStr; - } - else if (typeof(pItemIndexOrStr) == "string") - { - itemIndex = -1; - for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i) - { - if (this.txtItemList[i] == pItemIndexOrStr) - itemIndex = i; - } - } - else - return false; - - // Change the character in the item - var changedIt = false; - if ((itemIndex > -1) && (itemIndex < this.txtItemList.length)) - { - this.txtItemList[itemIndex] = chgCharInStr(this.txtItemList[itemIndex], pStrIndex, pNewText); - changedIt = true; - } - return changedIt; -} -function ChoiceScrollbox_GetChosenTextItemIndex() -{ - return this.chosenTextItemIndex; -} -function ChoiceScrollbox_SetItemArray(pArray, pStripCtrl) -{ - var safeToSet = false; - if (Object.prototype.toString.call(pArray) === "[object Array]") - { - if (pArray.length > 0) - safeToSet = (typeof(pArray[0]) == "string"); - else - safeToSet = true; // It's safe to set an empty array - } - - if (safeToSet) - { - delete this.txtItemList; - this.txtItemList = pArray; - - var stripCtrl = true; - if (typeof(pStripCtrl) == "boolean") - stripCtrl = pStripCtrl; - if (stripCtrl) - { - // Remove attribute/color characters from the text lines in the array - for (var i = 0; i < this.txtItemList.length; ++i) - this.txtItemList[i] = strip_ctrl(this.txtItemList[i]); - } - } - - return safeToSet; -} -function ChoiceScrollbox_ClearItems() -{ - this.txtItemList.length = 0; -} -function ChoiceScrollbox_SetEnterKeyOverrideFn(pOverrideFn) -{ - if (Object.prototype.toString.call(pOverrideFn) == "[object Function]") - this.enterKeyOverrideFn = pOverrideFn; -} -function ChoiceScrollbox_ClearEnterKeyOverrideFn() -{ - this.enterKeyOverrideFn = null; -} -function ChoiceScrollbox_AddInputLoopExitKey(pKeypress) -{ - this.inputLoopExitKeys[pKeypress] = true; -} -function ChoiceScrollbox_SetBottomBorderText(pText, pAddTChars, pAutoStripIfTooLong) -{ - if (typeof(pText) != "string") - return; - - const innerWidth = (pAddTChars ? this.dimensions.width-4 : this.dimensions.width-2); - - if (pAutoStripIfTooLong) - { - if (strip_ctrl(pText).length > innerWidth) - pText = pText.substr(0, innerWidth); - } - - // Re-build the bottom border string based on the new text - this.bottomBorder = "\x01n" + this.programCfgObj.colors.listBoxBorder + LOWER_LEFT_SINGLE; - if (pAddTChars) - this.bottomBorder += RIGHT_T_SINGLE; - if (pText.indexOf("\x01n") != 0) - this.bottomBorder += "\x01n"; - this.bottomBorder += pText + "\x01n" + this.programCfgObj.colors.listBoxBorder; - if (pAddTChars) - this.bottomBorder += LEFT_T_SINGLE; - var numCharsRemaining = this.dimensions.width - strip_ctrl(this.bottomBorder).length - 3; - for (var i = 0; i < numCharsRemaining; ++i) - this.bottomBorder += HORIZONTAL_SINGLE; - this.bottomBorder += LOWER_RIGHT_SINGLE; -} -function ChoiceScrollbox_DrawBorder() -{ - console.gotoxy(this.dimensions.topLeftX, this.dimensions.topLeftY); - console.print(this.topBorder); - // Draw the side border characters - var screenRow = this.dimensions.topLeftY + 1; - for (var screenRow = this.dimensions.topLeftY+1; screenRow <= this.dimensions.bottomRightY-1; ++screenRow) - { - console.gotoxy(this.dimensions.topLeftX, screenRow); - console.print(VERTICAL_SINGLE); - console.gotoxy(this.dimensions.bottomRightX, screenRow); - console.print(VERTICAL_SINGLE); - } - // Draw the bottom border - console.gotoxy(this.dimensions.topLeftX, this.dimensions.bottomRightY); - console.print(this.bottomBorder); -} -function ChoiceScrollbox_DrawInnerMenu(pSelectedIndex) -{ - var selectedIndex = (typeof(pSelectedIndex) == "number" ? pSelectedIndex : -1); - var startArrIndex = this.pageNum * this.numItemsPerPage; - var endArrIndex = startArrIndex + this.numItemsPerPage; - if (endArrIndex > this.txtItemList.length) - endArrIndex = this.txtItemList.length; - var selectedItemRow = this.dimensions.topLeftY+1; - var screenY = this.dimensions.topLeftY + 1; - for (var i = startArrIndex; i < endArrIndex; ++i) - { - console.gotoxy(this.dimensions.topLeftX+1, screenY); - if (i == selectedIndex) - { - printf(this.listIemHighlightFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth)); - selectedItemRow = screenY; - } - else - printf(this.listIemFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth)); - ++screenY; - } - // If the current screen row is below the bottom row inside the box, - // continue and write blank lines to the bottom of the inside of the box - // to blank out any text that might still be there. - while (screenY < this.dimensions.topLeftY+this.dimensions.height-1) - { - console.gotoxy(this.dimensions.topLeftX+1, screenY); - printf(this.listIemFormatStr, ""); - ++screenY; - } - - // Update the page number in the top border of the box. - console.gotoxy(this.pageNumTxtStartX, this.dimensions.topLeftY); - printf("\x01n" + this.programCfgObj.colors.listBoxBorderText + "Page %4d of %4d", this.pageNum+1, this.numPages); - return selectedItemRow; -} -function ChoiceScrollbox_RefreshOnScreen(pSelectedIndex) -{ - this.drawBorder(); - this.drawInnerMenu(pSelectedIndex); -} -function ChoiceScrollbox_RefreshItemCharOnScreen(pItemIndex, pCharIndex) -{ - if ((typeof(pItemIndex) != "number") || (typeof(pCharIndex) != "number")) - return; - if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length) || - (pItemIndex < this.topItemIndex) || (pItemIndex > this.bottomItemIndex)) - { - return; - } - if ((pCharIndex < 0) || (pCharIndex >= this.txtItemList[pItemIndex].length)) - return; - - // Save the current cursor position so that we can restore it later - const originalCurpos = console.getxy(); - // Go to the character's position on the screen and set the highlight or - // normal color, depending on whether the item is the currently selected item, - // then print the character on the screen. - const charScreenX = this.dimensions.topLeftX + 1 + pCharIndex; - const itemScreenY = this.dimensions.topLeftY + 1 + (pItemIndex - this.topItemIndex); - console.gotoxy(charScreenX, itemScreenY); - if (pItemIndex == this.chosenTextItemIndex) - console.print(this.programCfgObj.colors.listBoxItemHighlight); - else - console.print(this.programCfgObj.colors.listBoxItemText); - console.print(this.txtItemList[pItemIndex].charAt(pCharIndex)); - // Move the cursor back to where it was originally - console.gotoxy(originalCurpos); -} -function ChoiceScrollbox_DoInputLoop(pDrawBorder) -{ - var retObj = { - itemWasSelected: false, - selectedIndex: -1, - selectedItem: "", - lastKeypress: "" - }; - - // Don't do anything if the item list doesn't contain any items - if (this.txtItemList.length == 0) - return retObj; - - ////////////////////////////////// - // Locally-defined functions - - // This function returns the index of the bottommost item that - // can be displayed in the box. - // - // Parameters: - // pArray: The array containing the items - // pTopindex: The index of the topmost item displayed in the box - // pNumItemsPerPage: The number of items per page - function getBottommostItemIndex(pArray, pTopIndex, pNumItemsPerPage) - { - var bottomIndex = pTopIndex + pNumItemsPerPage - 1; - // If bottomIndex is beyond the last index, then adjust it. - if (bottomIndex >= pArray.length) - bottomIndex = pArray.length - 1; - return bottomIndex; - } - - - - ////////////////////////////////// - // Code - - // Variables for keeping track of the item list - this.numItemsPerPage = this.dimensions.height - 2; - this.topItemIndex = 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. - this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage); - this.numPages = Math.ceil(this.txtItemList.length / this.numItemsPerPage); - const topIndexForLastPage = (this.numItemsPerPage * this.numPages) - this.numItemsPerPage; - - if (pDrawBorder) - this.drawBorder(); - - // User input loop - // For the horizontal location of the page number text for the box border: - // Based on the fact that there can be up to 9999 text replacements and 10 - // per page, there will be up to 1000 pages of replacements. To write the - // text, we'll want to be 20 characters to the left of the end of the border - // of the box. - this.pageNumTxtStartX = this.dimensions.topLeftX + this.dimensions.width - 19; - this.maxItemWidth = this.dimensions.width - 2; - this.pageNum = 0; - var startArrIndex = 0; - this.chosenTextItemIndex = retObj.selectedIndex = 0; - var endArrIndex = 0; // One past the last array item - var curpos = { // For keeping track of the current cursor position - x: 0, - y: 0 - }; - var refreshList = true; // For screen redraw optimizations - this.continueInputLoopOverride = true; - var continueOn = true; - while (continueOn && this.continueInputLoopOverride) - { - if (refreshList) - { - this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage); - - // Write the list of items for the current page. Also, drawInnerMenu() - // will return the selected item row. - var selectedItemRow = this.drawInnerMenu(retObj.selectedIndex); - - // Just for sane appearance: Move the cursor to the first character of - // the currently-selected row and set the appropriate color. - curpos.x = this.dimensions.topLeftX+1; - curpos.y = selectedItemRow; - console.gotoxy(curpos.x, curpos.y); - console.print(this.programCfgObj.colors.listBoxItemHighlight); - - refreshList = false; - } - - // Get a key from the user (upper-case) and take action based upon it. - retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOSPIN, this.programCfgObj); - switch (retObj.lastKeypress) - { - case 'N': // Next page - case KEY_PAGE_DOWN: - refreshList = (this.pageNum < this.numPages-1); - if (refreshList) - { - ++this.pageNum; - this.topItemIndex += this.numItemsPerPage; - this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex; - // Note: this.bottomItemIndex is refreshed at the top of the loop - } - break; - case 'P': // Previous page - case KEY_PAGE_UP: - refreshList = (this.pageNum > 0); - if (refreshList) - { - --this.pageNum; - this.topItemIndex -= this.numItemsPerPage; - this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex; - // Note: this.bottomItemIndex is refreshed at the top of the loop - } - break; - case 'F': // First page - refreshList = (this.pageNum > 0); - if (refreshList) - { - this.pageNum = 0; - this.topItemIndex = 0; - this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex; - // Note: this.bottomItemIndex is refreshed at the top of the loop - } - break; - case 'L': // Last page - refreshList = (this.pageNum < this.numPages-1); - if (refreshList) - { - this.pageNum = this.numPages-1; - this.topItemIndex = topIndexForLastPage; - this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex; - // Note: this.bottomItemIndex is refreshed at the top of the loop - } - break; - case KEY_UP: - // Move the cursor up one item - if (retObj.selectedIndex > 0) - { - // If the previous item index is on the previous page, then we'll - // want to display the previous page. - var previousItemIndex = retObj.selectedIndex - 1; - if (previousItemIndex < this.topItemIndex) - { - --this.pageNum; - this.topItemIndex -= this.numItemsPerPage; - // Note: this.bottomItemIndex is refreshed at the top of the loop - refreshList = true; - } - else - { - // Display the current line un-highlighted - console.gotoxy(this.dimensions.topLeftX+1, curpos.y); - printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - // Display the previous line highlighted - curpos.x = this.dimensions.topLeftX+1; - --curpos.y; - console.gotoxy(curpos); - printf(this.listIemHighlightFormatStr, this.txtItemList[previousItemIndex].substr(0, this.maxItemWidth)); - console.gotoxy(curpos); // Move the cursor into place where it should be - refreshList = false; - } - this.chosenTextItemIndex = retObj.selectedIndex = previousItemIndex; - } - break; - case KEY_DOWN: - // Move the cursor down one item - if (retObj.selectedIndex < this.txtItemList.length - 1) - { - // If the next item index is on the next page, then we'll want to - // display the next page. - var nextItemIndex = retObj.selectedIndex + 1; - if (nextItemIndex > this.bottomItemIndex) - { - ++this.pageNum; - this.topItemIndex += this.numItemsPerPage; - // Note: this.bottomItemIndex is refreshed at the top of the loop - refreshList = true; - } - else - { - // Display the current line un-highlighted - console.gotoxy(this.dimensions.topLeftX+1, curpos.y); - printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - // Display the previous line highlighted - curpos.x = this.dimensions.topLeftX+1; - ++curpos.y; - console.gotoxy(curpos); - printf(this.listIemHighlightFormatStr, this.txtItemList[nextItemIndex].substr(0, this.maxItemWidth)); - console.gotoxy(curpos); // Move the cursor into place where it should be - refreshList = false; - } - this.chosenTextItemIndex = retObj.selectedIndex = nextItemIndex; - } - break; - case KEY_HOME: // Go to the first row in the box - if (retObj.selectedIndex > this.topItemIndex) - { - // Display the current line un-highlighted - console.gotoxy(this.dimensions.topLeftX+1, curpos.y); - printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - // Select the top item, and display it highlighted. - this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex; - curpos.x = this.dimensions.topLeftX+1; - curpos.y = this.dimensions.topLeftY+1; - console.gotoxy(curpos); - printf(this.listIemHighlightFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - console.gotoxy(curpos); // Move the cursor into place where it should be - refreshList = false; - } - break; - case KEY_END: // Go to the last row in the box - if (retObj.selectedIndex < this.bottomItemIndex) - { - // Display the current line un-highlighted - console.gotoxy(this.dimensions.topLeftX+1, curpos.y); - printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - // Select the bottommost item, and display it highlighted. - this.chosenTextItemIndex = retObj.selectedIndex = this.bottomItemIndex; - curpos.x = this.dimensions.topLeftX+1; - curpos.y = this.dimensions.bottomRightY-1; - console.gotoxy(curpos); - printf(this.listIemHighlightFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth)); - console.gotoxy(curpos); // Move the cursor into place where it should be - refreshList = false; - } - break; - case KEY_ENTER: - // If the enter key override function is set, then call it and pass - // this object into it. Otherwise, just select the item and quit. - if (this.enterKeyOverrideFn !== null) - this.enterKeyOverrideFn(this); - else - { - retObj.itemWasSelected = true; - // Note: retObj.selectedIndex is already set. - retObj.selectedItem = this.txtItemList[retObj.selectedIndex]; - refreshList = false; - continueOn = false; - } - break; - case KEY_ESC: // Quit - case CTRL_A: // Quit - case 'Q': // Quit - this.chosenTextItemIndex = retObj.selectedIndex = -1; - refreshList = false; - continueOn = false; - break; - default: - // If the keypress is an additional key to exit the input loop, then - // do so. - if (this.inputLoopExitKeys.hasOwnProperty(retObj.lastKeypress)) - { - this.chosenTextItemIndex = retObj.selectedIndex = -1; - refreshList = false; - continueOn = false; - } - else - { - // Unrecognized command. Don't refresh the list of the screen. - refreshList = false; - } - break; - } - } - - this.continueInputLoopOverride = true; // Reset - - console.attributes = "N"; // To prevent outputting highlight colors, etc.. - return retObj; -} - -/////////////////////////////////////////////////////////////////////////////////// - -// Writes a default twitlist for the user if it doesn't exist -function writeDefaultUserTwitListIfNotExist() -{ - if (file_exists(gUserTwitListFilename)) - return; - - var outFile = new File(gUserTwitListFilename); - if (outFile.open("w")) - { - outFile.writeln("; This is a personal twitlist for Digital Distortion Message Reader to block"); - outFile.writeln("; messages from (and to) certain usernames. The intention is that if you are"); - outFile.writeln("; being harassed by a specific person, or you simply do not wish to see their"); - outFile.writeln("; messages, you can filter that person by adding their name (or email address)"); - outFile.writeln("; to this file."); - - outFile.close(); - } -} - -// Returns whether 2 arrays have the same values -function arraysHaveSameValues(pArray1, pArray2) -{ - if (pArray1 == null && pArray2 == null) - return true; - else if (pArray1 != null && pArray2 == null) - return false; - else if (pArray1 == null && pArray2 != null) - return false; - - var arraysHaveSameValues = true; - if (pArray1.length != pArray2.length) - arraysHaveSameValues = false; - else - { - for (var a1i = 0; a1i < pArray1.length && arraysHaveSameValues; ++a1i) - { - var seenInArray2 = false; - for (var a2i = 0; a2i < pArray2.length && !seenInArray2; ++a2i) - seenInArray2 = (pArray2[a2i] == pArray1[a1i]); - arraysHaveSameValues = seenInArray2; - } - } - return arraysHaveSameValues; -} - -// Returns whether or not the sender of a message is a sysop. -// -// Parameters: -// pMsgHdr: A message header -// -// Return value: Boolean: Whether or not the sender of the message is a sysop -function msgSenderIsASysop(pMsgHdr) -{ - if (typeof(pMsgHdr) !== "object") - return false; - - var senderUserNum = 0; - if (pMsgHdr.hasOwnProperty("sender_userid")) - senderUserNum = system.matchuser(pMsgHdr.sender_userid); - else if (pMsgHdr.hasOwnProperty("from")) - { - senderUserNum = system.matchuser(pMsgHdr.from); - if (senderUserNum < 1) - senderUserNum = system.matchuserdata(U_NAME, pMsgHdr.from); - } - - var senderIsSysop = false; - if (senderUserNum >= 1) - { - if (senderUserNum == 1) - senderIsSysop = true; - else - { - var senderUser = new User(senderUserNum); - senderIsSysop = senderUser.is_sysop; - } - } - return senderIsSysop; -} - -// Gets the quote wrap settings for an external editor -// -// Parameters: -// pEditorCode: The internal code of an external editor -// -// Return value: An object containing the following properties: -// quoteWrapEnabled: Boolean: Whether or not quote wrapping is enabled for the editor -// quoteWrapCols: The number of columns to wrap quote lines -// If the given editor code is not found, quoteWrapEnabled will be false and quoteWrapCols will be -1 -function getExternalEditorQuoteWrapCfgFromSCFG(pEditorCode) -{ - var retObj = { - quoteWrapEnabled: false, - quoteWrapCols: -1 - }; - - if (typeof(pEditorCode) !== "string") - return retObj; - if (pEditorCode.length == 0) - return retObj; - - var editorCode = pEditorCode.toLowerCase(); - if (!xtrn_area.editor.hasOwnProperty(editorCode)) - return retObj; - - // Set up a cache so that we don't have to keep repeatedly parsing the Synchronet - // config every time the user replies to a message - if (typeof(getExternalEditorQuoteWrapCfgFromSCFG.cache) === "undefined") - getExternalEditorQuoteWrapCfgFromSCFG.cache = {}; - // If we haven't looked up the quote wrap cols setting yet, then do so; otherwise, use the - // cached setting. - if (!getExternalEditorQuoteWrapCfgFromSCFG.cache.hasOwnProperty(editorCode)) - { - if ((xtrn_area.editor[editorCode].settings & XTRN_QUOTEWRAP) == XTRN_QUOTEWRAP) - { - retObj.quoteWrapEnabled = true; - retObj.quoteWrapCols = console.screen_columns - 1; - - // For Synchronet 3.20 and newer, read the quote wrap setting from xtrn.ini - if (system.version_num >= 32000) - { - // The INI section for the editor should be something like [editor:SLYEDICE], and - // it should have a quotewrap_cols property - var xtrnIniFile = new File(system.ctrl_dir + "xtrn.ini"); - if (xtrnIniFile.open("r")) - { - var quoteWrapCols = xtrnIniFile.iniGetValue("editor:" + pEditorCode.toUpperCase(), "quotewrap_cols", console.screen_columns - 1); - if (quoteWrapCols > 0) - retObj.quoteWrapCols = quoteWrapCols; - xtrnIniFile.close(); - } - } - else - { - // Synchronet below version 3.20: Read the quote wrap setting from xtrn.cnf - var cnflib = load({}, "cnflib.js"); - var xtrnCnf = cnflib.read("xtrn.cnf"); - if (typeof(xtrnCnf) === "object") - { - for (var i = 0; i < xtrnCnf.xedit.length; ++i) - { - if (xtrnCnf.xedit[i].code.toLowerCase() == editorCode) - { - if (xtrnCnf.xedit[i].hasOwnProperty("quotewrap_cols")) - { - if (xtrnCnf.xedit[i].quotewrap_cols > 0) - retObj.quoteWrapCols = xtrnCnf.xedit[i].quotewrap_cols; - } - break; - } - } - } - } - } - getExternalEditorQuoteWrapCfgFromSCFG.cache[editorCode] = retObj; - } - else - retObj = getExternalEditorQuoteWrapCfgFromSCFG.cache[editorCode]; - - return retObj; -} - -// Changes a character in a string, and returns the new string. If any of the -// parameters are invalid, then the original string will be returned. -// -// Parameters: -// pStr: The original string -// pCharIndex: The index of the character to replace -// pNewText: The new character or text to place at that position in the string -// -// Return value: The new string -function chgCharInStr(pStr, pCharIndex, pNewText) -{ - if (typeof(pStr) != "string") - return ""; - if ((pCharIndex < 0) || (pCharIndex >= pStr.length)) - return pStr; - if (typeof(pNewText) != "string") - return pStr; - - return (pStr.substr(0, pCharIndex) + pNewText + pStr.substr(pCharIndex+1)); -} - -// Given a string of attribute characters, this function inserts the control code -// in front of each attribute character and returns the new string. -// -// Parameters: -// pAttrCodeCharStr: A string of attribute characters (i.e., "YH" for yellow high) -// -// Return value: A string with the control character inserted in front of the attribute characters -function attrCodeStr(pAttrCodeCharStr) -{ - if (typeof(pAttrCodeCharStr) !== "string") - return ""; - - var str = ""; - // See this page for Synchronet color attribute codes: - // http://wiki.synchro.net/custom:ctrl-a_codes - for (var i = 0; i < pAttrCodeCharStr.length; ++i) - { - var currentChar = pAttrCodeCharStr.charAt(i); - if (/[krgybmcwKRGYBMCWHhIiEeFfNn01234567]/.test(currentChar)) - str += "\x01" + currentChar; - } - return str; -} - -// Replaces @-codes in a string and removes any newlines and carriage returns from the end -// of the string -// -// Parameters: -// pText: The text to modify -// -// Return value: The text with @-codes replaced and newlines & carriage returns removed -// from the end of the text -function replaceAtCodesAndRemoveCRLFs(pText) -{ - if (typeof(pText) !== "string") - return ""; - - var formattedText = replaceAtCodesInStr(pText); - formattedText = word_wrap(formattedText, console.screen_columns-1, formattedText.length, false).replace(/\r|\n/g, "\r\n"); - while (formattedText.lastIndexOf("\r\n") == formattedText.length-2) - formattedText = formattedText.substr(0, formattedText.length-2); - while (formattedText.lastIndexOf("\r") == formattedText.length-1) - formattedText = formattedText.substr(0, formattedText.length-1); - while (formattedText.lastIndexOf("\n") == formattedText.length-1) - formattedText = formattedText.substr(0, formattedText.length-1); - return formattedText; -} - -function getLatestPostTimeWithMsgbase(pMsgbase, pSubCode) -{ - if (typeof(pMsgbase) !== "object") - return 0; - if (!pMsgbase.is_open) - return 0; - - var latestMsgTimestamp = 0; - if (pMsgbase.total_msgs > 0) - { - var msgIdx = pMsgbase.total_msgs - 1; - var msgHeader = pMsgbase.get_msg_header(true, msgIdx, false); - while (!isReadableMsgHdr(msgHeader, pSubCode) && (msgIdx >= 0)) - { - // TODO: I think we should be able to call get_msg_header() and get valid vote information, - // but that doesn't seem to be the case: - msgHeader = pMsgbase.get_msg_header(true, --msgIdx, true, true); - } - if (this.msgAreaList_lastImportedMsg_showImportTime) - latestMsgTimestamp = msgHeader.when_imported_time; - else - { - var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalTime != -1) - latestMsgTimestamp = msgWrittenTimeToLocalBBSTime(msgHeader); - else - latestMsgTimestamp = msgHeader.when_written_time; - } - } - return latestMsgTimestamp; -} - -// Given a messagebase and message header object, this returns a mode flag for -// use with console.print() to affect the character set based on the terminal. -function msg_pmode(pMsgbase, pMsgHdr) -{ - var pmode = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8 ? P_UTF8 : P_NONE; - if (pMsgHdr.from_ext !== "1") - pmode |= P_NOATCODES; - if (pMsgbase.cfg) - { - pmode |= pMsgbase.cfg.print_mode; - pmode &= ~pMsgbase.cfg.print_mode_neg; - } - return pmode; -} - -// Returns whether a value is a valid scan scope value. -function isValidScanScopeVal(pScanScope) -{ - return (typeof(pScanScope) === "number" && (pScanScope == SCAN_SCOPE_SUB_BOARD || pScanScope == SCAN_SCOPE_GROUP || pScanScope == SCAN_SCOPE_ALL)); -} - -// Lets the user (if they're a sysop) apply a quick-validation value (from SCFG > System > Security Options > Quick-Validation Values) -// to a user by username -// -// Parameters: -// pUsername: The name of the user to apply the quick-validation set to -// pUseANSI: Optional boolean - Whether or not to use ANSI -// pQuickValSetIdx: Optional - The index of the quick validation set to apply (0-9, as they -// appear in SCFG). If this is omitted, a menu will be displayed to allow -// choosing one of them -// -// Return value: An object containing the following properties: -// needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be -// refreshed (i.e., when the user has edited their twitlist) -// refreshBottomLine: Boolean - Whether or not the bottom line on the screen needs to be refreshed -// optionBoxTopLeftX: The top-left screen column of the option box (0 if none was used) -// optionBoxTopLeftY: The top-left screen row of the option box (0 if none was used) -// optionBoxWidth: The width of the option box (0 if none was used) -// optionBoxHeight: The height of the option box (0 if none was used) -function quickValidateLocalUser(pUsername, pUseANSI, pQuickValSetIdx) -{ - var retObj = { - needWholeScreenRefresh: false, - refreshBottomLine: false, - optionBoxTopLeftX: 0, - optionBoxTopLeftY: 0, - optionBoxWidth: 0, - optionBoxHeight: 0 - }; - - if (!user.is_sysop) - return retObj; - if (typeof(pUsername) !== "string" || pUsername == "") - return retObj; - - var useANSI = typeof(pUseANSI) === "boolean" ? pUseANSI : console.term_supports(USER_ANSI); - - var userNum = system.matchuser(pUsername); - if (userNum == 0) - { - var msgText = bbs.text(UnknownUser).replace(/\r|\n/g, ""); // Or UNKNOWN_USER - msgText += "\x01."; // Delay for 2 seconds - if (useANSI) - { - retObj.refreshBottomLine = true; - console.gotoxy(1, console.screen_columns); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_columns); - console.attributes = msgAttrs; - console.putmsg(msgText); - } - else - { - console.attributes = "N"; - console.crlf(); - console.putmsg(msgText); - } - return retObj; - } - - - // Get an array of the quick-validation values from SCFG - var quickValidationVals = getQuickValidationVals(); - // If pQuickValSetIdx is a number specifying a valid index, then use it; otherwise, display - // a menu of the quick-validation values to choose from - var quickValidationValSet = null; - var displayedMenu = false; - if (typeof(pQuickValSetIdx) === "number" && pQuickValSetIdx >= 0 && pQuickValSetIdx < quickValidationVals.length) - quickValidationValSet = quickValidationVals[pQuickValSetIdx]; - else - { - // No valid validation set index given; display the menu - var menuX = 2; - var menuY = 3; - var valHdrLineWithUsername = "Quick validation for " + pUsername; - var menuHdrStr = " Level E 1 2 3 4 C E R"; - console.attributes = "N"; - if (useANSI) - { - menuX = 25; - menuY = 12; - } - else - { - menuX = 2; - menuY = 3; - menuHdrStr = " " + menuHdrStr; - console.clear("\x01n"); - console.print(valHdrLineWithUsername); - console.crlf(); - console.print("Quick-Validation sets:"); - console.crlf(); - console.print(menuHdrStr); - console.crlf(); - } - // Create the menu of quick-validation sets - var valSetMenu = makeQuickValidationValLightbarMenu(useANSI, menuX, menuY, quickValidationVals); - // If using ANSI, draw some border characters at the left, bottom, and right sides of the menu - if (useANSI) - { - // Screen refresh values for returning from this function - retObj.needWholeScreenRefresh = false; - retObj.optionBoxTopLeftX = valSetMenu.pos.x-1; - retObj.optionBoxTopLeftY = valSetMenu.pos.y-4; - retObj.optionBoxWidth = valSetMenu.size.width+2; - retObj.optionBoxHeight = valSetMenu.size.height+5; - retObj.refreshBottomLine = false; - - // Display the menu - console.attributes = "GH"; - // Top border - var screenRow = valSetMenu.pos.y - 4; - console.gotoxy(valSetMenu.pos.x-1, screenRow); - console.print(UPPER_LEFT_DOUBLE); - for (var i = 0; i < valSetMenu.size.width; ++i) - console.print(HORIZONTAL_DOUBLE); - console.print(UPPER_RIGHT_DOUBLE); - // Side border characters - screenRow = valSetMenu.pos.y - 3; - var height = valSetMenu.size.height + 3; - for (var i = 0; i < height; ++i) - { - console.gotoxy(valSetMenu.pos.x-1, screenRow); - console.print(VERTICAL_DOUBLE); - console.gotoxy(valSetMenu.pos.x+valSetMenu.size.width, screenRow); - console.print(VERTICAL_DOUBLE); - ++screenRow; - } - // Bottom border characters - screenRow = valSetMenu.pos.y+valSetMenu.size.height; - console.gotoxy(valSetMenu.pos.x-1, screenRow); - console.print(LOWER_LEFT_DOUBLE); - for (var i = 0; i < valSetMenu.size.width; ++i) - console.print(HORIZONTAL_DOUBLE); - console.print(LOWER_RIGHT_DOUBLE); - console.attributes = "N"; - - console.gotoxy(menuX, menuY-3); - //printf("%-*s", valSetMenu.size.width, "Quick validation for:"); - printf("%-*s", valSetMenu.size.width, "Quick validation sets:"); - console.gotoxy(menuX, menuY-2); - printf("%-*s", valSetMenu.size.width, pUsername.substr(0, valSetMenu.size.width)); - console.gotoxy(menuX, menuY-1); - console.print(menuHdrStr); - } - else - { - retObj.needWholeScreenRefresh = true; - // Use green for the item color and high cyan for the item number color - valSetMenu.colors.itemColor = "\x01n\x01g"; - valSetMenu.colors.itemNumColor = "\x01n\x01c\x01h"; - } - quickValidationValSet = valSetMenu.GetVal(); - displayedMenu = true; - console.attributes = "N"; - } - var statusMsg = ""; - var msgAttrs = "N"; - if (quickValidationValSet != null && typeof(quickValidationValSet) === "object") - { - var userToEdit = new User(userNum); - /* - user.security properties - Name Type Ver Description - password string 3.10 password - password_date number 3.10 date password last modified (time_t format) - level number 3.10 security level (0-99) - flags1 number 3.10 flag set #1 (bitfield) can use +/-[A-?] notation - flags2 number 3.10 flag set #2 (bitfield) can use +/-[A-?] notation - flags3 number 3.10 flag set #3 (bitfield) can use +/-[A-?] notation - flags4 number 3.10 flag set #4 (bitfield) can use +/-[A-?] notation - exemptions number 3.10 exemption flags (bitfield) can use +/-[A-?] notation - restrictions number 3.10 restriction flags (bitfield) can use +/-[A-?] notation - credits number 3.10 credits - free_credits number 3.10 free credits (for today only) - minutes number 3.10 extra minutes (time bank) - extra_time number 3.10 extra minutes (for today only) - expiration_date number 3.10 expiration date/time (time_t format) - */ - // Each object in the returned array will have the following properties: - // level (numeric) - // expire - // flags1 - // flags2 - // flags3 - // flags4 - // credits - // exemptions - // restrictions - userToEdit.security.level = quickValidationValSet.level; - userToEdit.security.flags1 |= quickValidationValSet.flags1; - userToEdit.security.flags2 |= quickValidationValSet.flags2; - userToEdit.security.flags3 |= quickValidationValSet.flags3; - userToEdit.security.flags4 |= quickValidationValSet.flags4; - userToEdit.security.exemptions |= quickValidationValSet.exemptions; - userToEdit.security.restrictions |= quickValidationValSet.restrictions; - userToEdit.security.credits = quickValidationValSet.credits; - statusMsg = "Validation set applied"; - msgAttrs = "CH"; - } - else - { - statusMsg = "Aborted"; - msgAttrs = "YH"; - } - - // Display the final status message - if (useANSI) - { - if (displayedMenu) - { - // Clear the box on the screen and write that the validation set was applied to the user - var topBoxScreenRow = valSetMenu.pos.y - 3; - clearScreenRectangle(valSetMenu.pos.x, topBoxScreenRow, valSetMenu.size.width, valSetMenu.size.height+3); - console.gotoxy(valSetMenu.pos.x, topBoxScreenRow); - console.attributes = msgAttrs; - console.print(statusMsg + "\x01;\x01;"); - } - else - { - retObj.refreshBottomLine = true; - console.gotoxy(1, console.screen_columns); - console.cleartoeol("\x01n"); - console.gotoxy(1, console.screen_columns); - console.attributes = msgAttrs; - console.print(statusMsg + "\x01;\x01;"); - } - } - else - { - console.crlf(); - console.attributes = msgAttrs; - console.print(statusMsg + "\x01n\r\n\x01p"); - } - - console.attributes = "N"; - - return retObj; -} - -// Creates a DDLightbarMenu object to use for the quick-validation values. -// -// Parameters: -// pUseANSI: Boolean - Whether or not to enable the use of ANSI -// pMenuX: The top-left X coordinate for the menu -// pMenuY: The top-left Y coordinate for the menu -// pQuickValidationVals: An array of the quick-validation values from SCFG > System > Security Settings > Quick-Validation Values -// -// Return value: A DDLightbarMenu object to use for the quick-validation values menu -function makeQuickValidationValLightbarMenu(pUseANSI, pMenuX, pMenuY, pQuickValidationVals) -{ - var useANSI = (typeof(pUseANSI) === "boolean" ? pUseANSI : true); - - // Level E 1 2 3 4 C E R - // 60 Y Y Y Y Y Y Y Y - var quickValsMenuWidth = 22; //console.screen_columns - 4; - if (!console.term_supports(USER_ANSI)) - quickValsMenuWidth += 3; - var quickValsMenuHeight = pQuickValidationVals.length; - var quickValsMenu = new DDLightbarMenu(pMenuX, pMenuY, quickValsMenuWidth, quickValsMenuHeight); - quickValsMenu.AddAdditionalQuitKeys("qQ"); - quickValsMenu.scrollbarEnabled = true; - quickValsMenu.borderEnabled = false; - quickValsMenu.allowANSI = useANSI; - - //SetBorderChars(pBorderChars) - //"upperLeft", "upperRight", "lowerLeft", "lowerRight", "top", "bottom", "left", "right" - - var colors = { - level: "\x01n\x01w", - levelHi: "\x01n\x01w\x014", - YN: "\x01n\x01w", - YNHi: "\x01n\x01w\x014" - }; - var itemTextIdxes = { - levelStart: 0, - levelEnd: 7, - YN1Start: 7, - YN1End: 9, - YN2Start: 9, - YN2End: 11, - YN3Start: 11, - YN3End: 13, - YN4Start: 13, - YN4End: 15, - YN5Start: 15, - YN5End: 17, - YN6Start: 17, - YN6End: 19, - YN7Start: 19, - YN7End: 21, - YN8Start: 21, - YN8End: 23 - }; - - quickValsMenu.SetColors({ - itemColor: [{start: itemTextIdxes.levelStart, end: itemTextIdxes.levelEnd, attrs: colors.level}, - {start: itemTextIdxes.YN1Start, end: itemTextIdxes.YN1End, attrs: colors.YN}, - {start: itemTextIdxes.YN2Start, end: itemTextIdxes.YN2End, attrs: colors.YN}, - {start: itemTextIdxes.YN3Start, end: itemTextIdxes.YN3End, attrs: colors.YN}, - {start: itemTextIdxes.YN4Start, end: itemTextIdxes.YN4End, attrs: colors.YN}, - {start: itemTextIdxes.YN5Start, end: itemTextIdxes.YN5End, attrs: colors.YN}, - {start: itemTextIdxes.YN6Start, end: itemTextIdxes.YN6End, attrs: colors.YN}, - {start: itemTextIdxes.YN7Start, end: itemTextIdxes.YN7End, attrs: colors.YN}, - {start: itemTextIdxes.YN8Start, end: itemTextIdxes.YN8End, attrs: colors.YN}], - selectedItemColor: [{start: itemTextIdxes.levelStart, end: itemTextIdxes.levelEnd, attrs: colors.levelHi}, - {start: itemTextIdxes.YN1Start, end: itemTextIdxes.YN1End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN2Start, end: itemTextIdxes.YN2End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN3Start, end: itemTextIdxes.YN3End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN4Start, end: itemTextIdxes.YN4End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN5Start, end: itemTextIdxes.YN5End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN6Start, end: itemTextIdxes.YN6End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN7Start, end: itemTextIdxes.YN7End, attrs: colors.YNHi}, - {start: itemTextIdxes.YN8Start, end: itemTextIdxes.YN8End, attrs: colors.YNHi}] - }); - - quickValsMenu.quickValidationVals = pQuickValidationVals; - //format("%*s", this.numTabSpaces, "") - quickValsMenu.itemFormatStr = "%6d %s %s %s %s %s %s %s %s"; - quickValsMenu.NumItems = function() { - return this.quickValidationVals.length; - }; - quickValsMenu.GetItem = function(pItemIndex) { - var valYNStrs = [ - this.quickValidationVals[pItemIndex].expire > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].flags1 > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].flags2 > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].flags3 > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].flags4 > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].credits > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].exemptions > 0 ? CHECK_CHAR : " ", - this.quickValidationVals[pItemIndex].restrictions > 0 ? CHECK_CHAR : " " - ]; - - var menuItemObj = this.MakeItemWithRetval(-1); - menuItemObj.retval = this.quickValidationVals[pItemIndex]; - menuItemObj.text = format("%6d", this.quickValidationVals[pItemIndex].level); - for (var i = 0; i < valYNStrs.length; ++i) - menuItemObj.text += " " + valYNStrs[i]; - return menuItemObj; - }; - - return quickValsMenu; -} - -// Returns an array of the quick-validation sets configured in -// SCFG > System > Security > Quick-Validation Values. This reads -// from main.ini, which exists with Synchronet 3.20 and newer. -// In SCFG: -// -// Level 60 | -// Flag Set #1 | -// Flag Set #2 | -// Flag Set #3 | -// Flag Set #4 | -// Exemptions | -// Restrictions | -// Extend Expiration 0 days | -// Additional Credits 0 | -// -// Each object in the returned array will have the following properties: -// level (numeric) -// expire -// flags1 -// flags2 -// flags3 -// flags4 -// credits -// exemptions -// restrictions -function getQuickValidationVals() -{ - var validationValSets = []; - // In SCFG > System > Security > Quick-Validation Values, there are 10 sets of - // validation values. These are in main.ini as [valset:0] through [valset:9] - // This reads from main.ini, which exists with Synchronet 3.20 and newer. - //system.version_num >= 32000 - var mainIniFile = new File(system.ctrl_dir + "main.ini"); - if (mainIniFile.open("r")) - { - for (var i = 0; i < 10; ++i) - { - var valSection = mainIniFile.iniGetObject(format("valset:%d", i)); - if (valSection != null) - validationValSets.push(valSection); - } - mainIniFile.close(); - } - return validationValSets; -} - -// Clears a rectangle on the screen -function clearScreenRectangle(pX, pY, pWidth, pHeight) -{ - console.attributes = "N"; - var lastScreenRow = pY + pHeight - 1; - for (var screenRow = pY; screenRow <= lastScreenRow; ++screenRow) - { - console.gotoxy(pX, screenRow); - printf("%-*s", pWidth, ""); - } -} - - -// With a group & sub-board index, this function gets the date -// & time of the latest posted message from a sub-board (or group of sub-boards, if using -// sub-board name collapsing). This function also gets the description of the sub-board (or -// group of sub-boards if using sub-board name collapsing). -// -// Parameters: -// pGrpIdx: The index of the message group -// pSubIdx: The index of the sub-board in the message group (if using -// sub-board name collapsing, this could be the index of a set -// of sub-subboards). -// pShowImportTime: Boolean - Whether or not to use import time. If false, this will use the -// message written time. -// -// Return value: An object containing the following properties: -// desc: The description of the sub-board (or group of sub-subboards if using name collapsing) -// numItems: The number of messages in the sub-board or number of sub-subboards in the group, -// if using sub-board name collapsing -// subCode: The internal code of the sub-board (this will be an empty string if it's a group of sub-subboards) -// newestTime: A value containing the date & time of the newest post in the sub-board or group of sub-boards -function getSubBoardInfo(pGrpIdx, pSubIdx, pShowImportTime) -{ - var retObj = { - desc: "", - numItems: 0, - subCode: "", - newestTime: 0 - }; - - var showImportTime = (typeof(pShowImportTime) === "boolean" ? pShowImportTime : false); - - retObj.desc = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].description; - retObj.subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code; - // Get the number of messages in the sub-board - var numMsgs = numReadableMsgs(null, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code); - if (numMsgs > 0) - { - retObj.numItems = numMsgs; - //var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true); - var msgHeader = getLatestMsgHdr(retObj.subCode); - if (msgHeader != null) - { - // Set the newest post time - if (showImportTime) - retObj.newestTime = msgHeader.when_imported_time; - else - { - var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader); - if (msgWrittenLocalBBSTime != -1) - retObj.newestTime = msgWrittenLocalBBSTime; - else - retObj.newestTime = msgHeader.when_written_time; - } - } - } - - return retObj; -} - -// Gets the header of the latest readable message in a sub-board, -// given a number of messages to look at. -// -// Paramters: -// pSubCode: The internal code of the message sub-board -// -// Return value: The message header of the latest readable message. If -// none is found, this will be null. -function getLatestMsgHdr(pSubCode) -{ - var msgHdr = null; - var msgBase = new MsgBase(pSubCode); - if (msgBase.open()) - { - msgHdr = getLatestMsgHdrWithMsgbase(msgBase, pSubCode); - msgBase.close(); - } - return msgHdr; -} -// Gets the header of the latest readable message in a sub-board, -// given a number of messages to look at. -// -// Paramters: -// pMsgbase: A MsgBase object for the sub-board, already opened -// pSubCode: The internal code of the sub-board -// -// Return value: The message header of the latest readable message. If -// none is found, this will be null. -function getLatestMsgHdrWithMsgbase(pMsgbase, pSubCode) -{ - if (typeof(pMsgbase) !== "object") - return null; - if (!pMsgbase.is_open) - return null; - - // Look through the message headers to find the latest readable one - var msgHdrToReturn = null; - var msgIdx = pMsgbase.total_msgs-1; - var msgHeader = pMsgbase.get_msg_index(true, msgIdx, false); - while (!isReadableMsgHdr(msgHeader, pSubCode) && (msgIdx >= 0)) - msgHeader = pMsgbase.get_msg_index(true, --msgIdx, true); - if (msgHeader != null) - msgHdrToReturn = pMsgbase.get_msg_header(true, msgIdx, false); - return msgHdrToReturn; -} - -// For a sub-board, updates the user's newscan pointers for a sub-board so -// that there are no more new messages, and marks any messages written to -// the user as read. -// -// Parameters: -// pSubCode: The internal code for the sub-board -// -// Return value: Boolean - Whether or not this function was successful -function subBoardNewscanAllRead(pSubCode) -{ - if (pSubCode == "mail") - return false; - - var wasSuccessful = true; - var msgbase = new MsgBase(pSubCode); - if (msgbase.open()) - { - msg_area.sub[pSubCode].scan_ptr = msgbase.last_msg; - msg_area.sub[pSubCode].last_read = msgbase.last_msg; - - // Mark any unread messages to the user as read - var indexRecords = msgbase.get_index(); - if (indexRecords != null) - { - for (var i = 0; i < indexRecords.length; ++i) - { - if (msgIsToCurrentUserByName(indexRecords[i]) && !Boolean(indexRecords[i].attr & MSG_READ)) - { - var msgHdr = msgbase.get_msg_header(false, indexRecords[i].number, false); - if (msgHdr != null) - { - msgHdr.attr |= MSG_READ; - if (!msgbase.put_msg_header(false, indexRecords[i].number, msgHdr)) - wasSuccessful = false; - } - } - } - } - msgbase.close(); - } - else - wasSuccessful = false; - return wasSuccessful; -} - -// Hepler function for getting a message header: Opens a messagebase, calls -// get_msg_header(), closes the messagebase, and returns the header -function getMsgHdr(pSubCode, pByOffset, pNumOrOffset, pExpandFields, pIncludeVotes) -{ - var msgHdr = null; - var msgbase = new MsgBase(pSubCode); - if (msgbase.open()) - { - var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : true); - var includeVotes = (typeof(pIncludeVotes) === "boolean" ? pIncludeVotes : false); - msgHdr = msgbase.get_msg_header(pByOffset, pNumOrOffset, expandFields, includeVotes); - msgbase.close(); - } - return msgHdr; -} - -// Adds to the twit list. -// -// Parameters: -// pStr: A name/email or netmail address -// -// Return value: Boolean - Whether or not this was successful -function addToTwitList(pStr) -{ - if (typeof(pStr) !== "string") - return false; - - var wasSuccessful = true; - if (!entryExistsInTwitList(pStr)) - { - wasSuccessful = false; - var twitFile = new File(system.ctrl_dir + "twitlist.cfg"); - //if (twitFile.open(twitFile.exists ? "r+" : "w+")) - if (twitFile.open("a")) - { - wasSuccessful = twitFile.writeln(pStr); - twitFile.close(); - } - } - return wasSuccessful; -} -// Returns whether an entry exists in the twit list. -// -// Parameters: -// pStr: An entry to check in the twit list -// -// Return value: Boolean - Whether or not the given string exists in the twit list -function entryExistsInTwitList(pStr) -{ - if (typeof(pStr) !== "string") - return false; - - var entryExists = false; - var twitFile = new File(system.ctrl_dir + "twitlist.cfg"); - if (twitFile.open("r")) - { - while (!twitFile.eof && !entryExists) - { - //// Read the next line from the config file. - var fileLine = twitFile.readln(2048); - // fileLine should be a string, but I've seen some cases - // where for some reason it isn't. If it's not a string, - // then continue onto the next line. - 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; - - // See if this line matches the given string - entryExists = (pStr == skipsp(truncsp(fileLine))); - } - - twitFile.close(); - } - return entryExists; -} - -// Adds to the global email filter (email.can). -// -// Parameters: -// pEmailAddr: An email address -// -// Return value: Boolean - Whether or not this was successful -function addToGlobalEmailFilter(pEmailAddr) -{ - if (typeof(pEmailAddr) !== "string") - return false; - - var wasSuccessful = true; - if (!entryExistsInGlobalEmailFilter(pEmailAddr)) - { - wasSuccessful = false; - var filterFile = new File(system.text_dir + "email.can"); - //if (filterFile.open(filterFile.exists ? "r+" : "w+")) - if (filterFile.open("a")) - { - wasSuccessful = filterFile.writeln(pEmailAddr); - filterFile.close(); - } - } - return wasSuccessful; -} -// Returns whether an entry exists in the global email filter (email.can). -// -// Parameters: -// pEmailAddr: An entry to check in the twit list -// -// Return value: Boolean - Whether or not the given string exists in the twit list -function entryExistsInGlobalEmailFilter(pEmailAddr) -{ - if (typeof(pEmailAddr) !== "string") - return false; - - var entryExists = false; - var filterFile = new File(system.text_dir + "email.can"); - if (filterFile.open("r")) - { - while (!filterFile.eof && !entryExists) - { - //// Read the next line from the config file. - var fileLine = filterFile.readln(2048); - // fileLine should be a string, but I've seen some cases - // where for some reason it isn't. If it's not a string, - // then continue onto the next line. - 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; - - // See if this line matches the given string - entryExists = (pEmailAddr == skipsp(truncsp(fileLine))); - } - - filterFile.close(); - } - return entryExists; -} - -/////////////////////////////////////////////////////////////////////////////////// - -// For debugging: 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 = "\x01n"; - 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); - } -} diff --git a/readme.txt b/readme.txt deleted file mode 100644 index f0f8dec8d2d92fcef5487e8d03f3e611f827aa44..0000000000000000000000000000000000000000 --- a/readme.txt +++ /dev/null @@ -1,1373 +0,0 @@ - Digital Distortion Message Reader - Version 1.93a - Release date: 2024-01-07 - - by - - Eric Oulashin - Sysop of Digital Distortion - BBS internet address: digitaldistortionbbs.com - Alternate address: digdist.bbsindex.com - Email: eric.oulashin@gmail.com - - - -This file describes the Digital Distortion Message Reader. - -Contents -======== -1. Disclaimer -2. Introduction -3. Installation & Setup - - Loadable Modules setup - - Command shell setup - - Background: Running JavaScript scripts in Synchronet - - Command-line parameters - - Synchronet command shell background - - Installing into a command shell -4. Header ANSI/asc file -5. User avatars -6. Configuration file & color/text theme configuration file - - Main configuration file (DDMsgReader.cfg) - - Theme configuration file -7. Indexed reader mode -8. Quick-Validating users (while reading their message) -9. Drop file for replying to messages with Synchronet message editors -10. text.dat lines used in Digital Distortion Message Reader - - -1. Disclaimer -============= -I cannot guarantee that this script is 100% free of bugs. However, I have -tested it, and I used it often during development, so I have some confidence -that there are no serious issues with it (at least, none that I have seen). - - -2. Introduction -=============== -Digital Distortion Message Reader is a script for Synchronet that provides an -alternate message reading interface. For ANSI users, a reader interface is -provided which allows scrolling the message up & down (with the up & down arrow -keys, as well as the PageUp & PageDown keys), navigating through the messages -in the message area (AKA sub-board) using the left & right arrow keys, replying -to messages, and other common messagebase functionality. The Digital -Distortion Message Reader can also list messages in the message area and allows -forward & reverse navigation through the message list using a lightbar or -traditional user interface. An integrated message area chooser feature is also -included, allowing the user to change to a different message area to read/list -messages. Message newscan and various types of message searching are also -available. This script requires Synchronet version 3.15 or newer. - -When using Synchronet 3.17, the message voting features added in Synchronet -3.17 are supported. For regular messages, users can vote a message up or down, -and users can also vote in poll messages. - -If the user's terminal does not support ANSI, the reader will fall back to a -traditional user interface (which does not support scrolling). The user -interface style can also be toggled by the sysop in the configuration file -in case the sysop wants the reader to use the traditional interface even for -ANSI users. - -With this message reader, users can have their own personal twitlists. This -allows users to not see messages from (or to) specified usernames. A user can -edit their twit list by going into their user settings (with the Ctrl-U hotkey) -and choosing the option to edit their personal twit list. - -This reader effectively replaces the Digital Distortion Message Lister, which -provided a traditional message reader interface but not the scrollable reader -interface for ANSI users. - -Thanks goes to Accession/Access Denied (sysop of The Pharcyde) and Psi-Jack -(sysop of Decker's Heaven) for testing the reader while it was in development. - -The following is a list of features: -- Provides an enhanced, very functional yet intuitive user interface for - reading messages for ANSI users, including the ability to scroll the message, - navigate forward & back through the messages in the message area, reply to - messages, etc. If the user's terminal does not support ANSI, the reader will - fall back to a more traditional user interface (which, for instance, does not - allow scrolling the message up & down). -- Can be used to read message sub-boards or personal email -- Allows switching between the enhanced reader interface and the message list - to allow the user to browse messages & select another message to read. The - message list displays a formatted summary list of the messages in the user's - current message area and can be navigated forward & backward, and allows - selection of a message to read (basically, switching into reader mode). The - message list can be configured for a lightbar or traditional user interface - (the lightbar interface will only be available for ANSI users). -- Message scanning and searching is supported: New-message scan, new-to-you - scan, all-to-you scan, keyword search, from name search, and to name search -- Allows a custom message header ANSI/.asc file to be used in the reader mode. - If there is no custom header file, a default header style will be used. For - There can also be different custom header files for various terminal widths. -- Allows changing to a different message area from within the reader or message - list. The area chooser will use a lightbar interface for ANSI users or a - traditional interface for users whose terminal doesn't support ANSI. -- Allows the user to delete and edit existing messages that they've written, if the - sub-board supports those operations. -- Allows the user to download file attachments, whether uploaded to their - mailbox on Synchronet or attached to internet emails. When a message has - attachments, it will appear in the message list with an "A" between the - message number and sender name. -- Allows the user to forward a message to an email address or another user - (using the O key). This can be useful, for instance, if the user wants to - send a message in a public sub-board to their personal email for future - reference or send a message from a public sub-board to another user to - discuss the topic privately. -- Allows sysops to save a message to the BBS machine for future reference -- Allows sysops to edit the user account of the message author, if the user's - account exists on the BBS. This is done with the U key while reading a - message. This can be useful for BBSes that require new users to send a - message to the sysop when they sign up, in case the sysop needs to edit their - account. -- Allows the ability to batch-delete multiple messages. This is most useful, - for instance, if a user gets many spam emails in their personal inbox. Batch - deleting is only allowed when the user has permission to delete messages - (such as their own personal email). To batch-delete messages, the user can - select multiple messages (from the message list) and then press CTRL-D (from - the message list) to delete them. Messages can be selected in the following - ways: - o Lightbar message list: The spacebar selects an individual message. CTRL-A - lets the user select or un-select all messages. - o Traditional message list: The S key lets the user select or un-select - messages, by typing message numbers, A to select all, or N to select none - (un-select all). The list of message numbers is comma-separated or - space-separated, allowing for number ranges such as 120-130 for instance. - o Reader interface: The spacebar selects the message. -To delete the selected messages, the user must be in the message list; the -CTRL-D key combo is used for batch delete, and it will prompt the user for -confirmation before deleting the messages. -- The program settings, colors, and some text can be changed via configuration - files. The configuration files may be placed in the same directory as the - .js script or in the sbbs/ctrl directory. -- Allows a personal twit list, editable via user settings (Ctrl-U) -- Has an "indexed" mode, which displays a menu of sub-boards that includes the - total number of messages and number of new messages in each, and lets the user - choose a sub-board to read. This can be used for a regular "read", which - lists all sub-boards, or a newscan, which lists the sub-boards enabled for - newscan by the user. -- Allows the sysop to quick-validate a local user while reading one of their - messages. The hotkey to do so is Ctrl-Q. - -If a message has been marked for deletion, it will appear in the message list -with a blinking red asterisk (*) after the message number. - -When displaying a message to the user, this script will honor the attribute -code toggles set up under Synchronet's configuration program (SCFG), -under Message Options > Extra Attribute Codes. - -As the sysop, when reading a message, the hotkey Ctrl-O will show the operator -menu. Most of the operator menu items are already available, but the operator -menu also has the additional option to add the author of the message (the 'from' -name) to the twit list. - - -3. Installation & Setup -======================= -Digital Distortion Message Reader is comprised of the following files: -1. DDMsgReader.js The Digital Distortion Message Reader script - -2. DDMsgReader.cfg The reader configuration file - -3. ddmr_cfg.js A menu-driven configuration script to help with - changing configuration options. You can run it at a - command prompt in the DDMsgReader directory with the - following command: - jsexec ddmr_cfg - Alternately (with the filename extension): - jsexec ddmr_cfg.js - -4. DefaultTheme.cfg The default theme file containing colors & some - configurable text strings used in the reader - -5. ddmr_lm.js Loadable module script for setup in SCFG (only needed - for Synchronet 3.19 and earlier, or Synchronet built - before 2023-02-20) - -The configuration files are plain text files, so they can be edited using any -editor. - -The first 3 files (DDMsgReader.js, DDMsgReader.cfg, and DefaultTheme.cfg) can be -placed together in any directory. ddmr_lm.js is only needed if you're using -Synchronet 3.19 or earlier (or a Synchronet development build before -2023-02-20); if so, ddmr_lm.js should be copied to your sbbs/mods directory. - -The examples in this document will assume the first 3 files are in the -sbbs/xtrn/DDMsgReader directory. - -Loadable Modules setup ----------------------- -The easiest way to get Digital Distortion Message Reader set up is via the -Loadable Module options in SCFG > System > LOadable Modules. - -The Loadable Modules options let you specify scripts to run for various events -in Synchronet. As of Synchronet 3.19, the following Loadable Modules options -are available in SCFG for message reading/scanning events: -- Read Mail (added in Synchronet 3.16) -- Scan Msgs (added in Synchronet 3.16) -- Scan Subs (added in Synchronet 3.16) -- List Msgs (added in Synchronet 3.18) - -The Loadable Modules options take the filename of the script (sometimes without -the filename extension). - -Depending on your Synchronet version and where you have DDMsgReader.js, you may -be able to specify DDMsgReader.js directly as the loadable module for the above -settings. However, if you're using an earlier verison of Synchronet (before -3.20) and you have DDMsgReader.js in ../xtrn/DDMsgReader or another path, you -will need to use ddmr_lm.js. Refer to one of the below sections, depending on -which version of Synchronet you're using. - -For Synchronet 3.20 (& builds from 2023-02-20) and newer --------------------------------------------------------- -As of Synchronet 3.20, Synchronet allows up to 63 characters with a full -command-line for a loadable module, allowing DDMsgReader.js to be specified -directly as the loadable module for the 4 entries mentioned above. I've noticed -that it must include the .js filename extension. For example, if you have -DDMsgReader.js in ../xtrn/DDMsgReader, you would specify the following for all -4 of the above loadable module settings: - - Read Mail ../xtrn/DDMsgReader/DDMsgReader.js - Scan Msgs ../xtrn/DDMsgReader/DDMsgReader.js - Scan Subs ../xtrn/DDMsgReader/DDMsgReader.js - List Msgs ../xtrn/DDMsgReader/DDMsgReader.js - -For Synchronet 3.19 (& builds before 2023-02-20) ------------------------------------------------- -For older versions of Synchronet, the Loadable Modules options don't allow a -leading path in front of the name; also, many/most loadable modules were limited -to just 8 characters. So, if you have DDMsgReader.js in a path other than -sbbs/exec or sbbs/mods, one solution is to copy the included ddmr_lm.js to -either your sbbs/exec or sbbs/mods directory (ideally sbbs/mods so it wouldn't -get accidentally deleted) and specify ddmr_lm in your Loadable Modules as -follows: - - Read Mail ddmr_lm - Scan Msgs ddmr_lm - Scan Subs ddmr_lm - List Msgs ddmr_lm - -Also, if you will be running the script from a directory other than -xtrn/DDMsgReader, edit ddmr_lm.js and look for the text "SYSOPS:" (without the -double-quotes). One or two lines below that, there is a variable called -msgReaderPath - Change that so that it contains the path where you copied -DDMsgReader.js. - -Alternately, you can copy DDMsgReader.js to your sbbs/exec or sbbs/mods -directory and specify DDMsgReader in your Loadable Modules for the above -modules in SCFG. For that to work, you would also need to copy DDMsgReader.cfg -to your sbbs/ctrl directory or to sbbs/mods along with DDMsgReader.js. - -There are a few search modes that Synchronet provides that Digital Distortion -Message Reader doesn't support yet (such as continuous newscan and browse new -scan), and for those situations, the Loadable Modules scripts will fall back to -the stock Synchronet behavior. - - -Command shell setup -------------------- -Digital Distortion Message Reader can be set up by adding options to your -command shell to run DDMsgReader.js for any or all of the desired functionality -(reading, searching, message scanning, etc). The command-line parameters are -described in the subsection "Command-line parameters". Installing into a -command shell is described in the subsection "Installing into a command shell". - -Background: Running JavaScript scripts in Synchronet ----------------------------------------------------- -The general syntax for a command to run a JavaScript script in Synchronet is -with a question mark before the .js file. For example: -?../xtrn/DDMsgReader/DDMsgReader.js - -In a Baja script, you can use the 'exec' command to run a JavaScript script, as -in the following example: -exec "?../xtrn/DDMsgReader/DDMsgReader.js" - -In a JavaScript script, you can use the bbs.exec() function to run a JavaScript -script, as in the following example: -bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js"); - -Alternately, the reader can be installed as an external program (in SCFG in -External Programs > Online Programs (Doors)). See the following document for -more information: -http://wiki.synchro.net/howto:door:index?s[]=doors - -Command-line parameters ------------------------ -The Digital Distortion Message Reader supports command-line parameters to -specify some behavior options. The command-line parameters are used in the -command string after DDMsgReader.js. - -Most of the command-line arguments are in -arg=val format, where arg is the -argument (parameter) name, and val is the value for that argument. Some of -the command-line parameters are simply in -arg format, to enable an option. -For example, -search=new_msg_scan will start the reader to do a new message -scan. Another example is -personalEmail which lets the user read their -personal email. - -The following are the command-line parameters supported by DDMsgReader.js: --indexedMode: Starts DDMsgreader in "indexed" reader mode, which lists all - sub-boards, with total number of messages, number of new messages, - and last post date, allowing the user to select a sub-board to - read. This will prompt the user for "Group or All": Whether the - user wants to list sub-boards in the current group, or all - sub-boards. - This is intended to work if it is the only command-line 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. This overrides the startMode option in the - confiruation file. Available options are read (or reader) for - reader mode and list (or lister) for message list mode. --configFilename: Specifies the name of the configuration file to use. Defaults - to DDMsgReader.cfg. --subBoard: The sub-board (internal code or number) to read, other than the - user's current sub-board. This is optional; if this is specified, - the sub-board specified by this option will be used instead of the - user's current sub-board. If this option is specified, the - -chooseAreaFirst option will be ignored. --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 option - 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. --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. - -The following parameters generally shouldn't be used unless you know what -you're doing. These were added for use by the Loadable Modules scripts, which -Synchronet will load for various scenarios: - --userNum: Specify a user number for reading personal email. This parameter is - there because although usually the current user will be reading their - own personal mail, there are situations where a sysop can read other - users' personal mail. --allPersonalEmail: Read all personal email (to/from all). There are instances - where Synchronet supports this, but more than likely only - for sysops. - -Synchronet command shell background ------------------------------------ -If you are already familiar with Synchronet's command shell concepts, you can -skip to the subsection "Installing into a command shell" below. If you are not -yet familiar with how Synchronet's menus are controlled, the key is that -Synchronet doesn't have a menu editor like some other BBS packages do. -Instead, Synchronet uses a "command shell", which is a script that runs when a -user logs in and controls the flow of activity as it responds to the user's -commands. Synchronet supports two languages for its scripts: Baja and -JavaScript. Baja is Synchronet's own scripting language; JavaScript is an -industry standard scripting language that Synchronet has provided extensions -for to allow scripting while also being able to take advantage of some of -JavaScript's other features. Baja scripts need to be compiled before running, -whereas JavaScript scripts don't. For more information on Synchronet command -shells & scripting, see the following documentation: -- Synchronet command shells: -http://wiki.synchro.net/custom:command_shell -- Synchronet modules: -http://wiki.synchro.net/module:index?s[]=scripts -- Baja language reference: -http://www.synchro.net/docs/baja.html -- Synchronet's JavaScript object reference (it's best to become familiar with - JavaScript before referring to this document): -http://www.synchro.net/docs/jsobjs.html - -Installing into a command shell -------------------------------- -Examples for running the reader will assume that it's installed in the -directory sbbs/xtrn/DDMsgReader. - -If you are unsure which command shell you are using, you are likely using -Synchronet's "default" command shell, which is contained in the files -default.src and default.bin in the synchronet/exec directory. To modify the -default command shell, you'll need to open default.src with a text editor. -These are the key things to modify in default.src: -- Search for msg_read and replace that with the following: - exec "?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read" -- Where msg_your_scan appears, run DDMsgReader.js with the following: - exec "?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_new_scan -startMode=read" -- Where msg_your_scan_all appears, run DDMsgReader.js with the command-line - exec "?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_new_scan_all -startMode=read" - -If you are using a JavaScript command shell, the process would be similar. You -will need to determine where the current message operations are done in the -shell and replace them with the appropriate commands for running -DDMsgReader.js. - -The following are example command line strings for running the reader to -perform some common message operations. These command lines can be used with -the exec command in Baja, bbs.exec() method in JavaScript, or as set up as an -external door in SCFG. This list is not complete but provides examples of some -common message operations. - -- Read messages in the current sub-board: -?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read - -- List messages in the current sub-board: -?../xtrn/DDMsgReader/DDMsgReader.js -startMode=list - -- New message scan: -?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=new_msg_scan - -- New-to-user message scan (scan for new messages to the user): -?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=to_user_new_scan - -- Scan for all messages to the user: -?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=to_user_all_scan - -- Start in indexed reader mode: -?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode - -- Text (keyword) search in the current sub-board, and list the messages found: -?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -startMode=list - -- 'From' name message search in the current sub-board, and list messages found: -?../xtrn/DDMsgReader/DDMsgReader.js -search=from_name_search -startMode=list - -- 'To' name message search in the current sub-board, and list messages found: -?../xtrn/DDMsgReader/DDMsgReader.js -search=to_name_search -startMode=list - -- Search for all messages to the logged-in user in the current sub-board, and - list the messages found: -?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_search -startMode=list - -- Read personal email: -?../xtrn/DDMsgReader/DDMsgReader.js -personalEmail -startMode=read - -- List personal email: -?../xtrn/DDMsgReader/DDMsgReader.js -personalEmail -startMode=list - -- Read sent personal email: -?../xtrn/DDMsgReader/DDMsgReader.js -personalEmailSent -startMode=read - -- Search personal email with a keyword, and start with the message list: -?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -personalEmail -startMode=list - -Alternately, for searching personal email with a keyword, you can specify -subBoard=mail -instead of -personalEmail: -?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -subBoard=mail -startMode=list - - -Text customization using text.dat ---------------------------------- -Digital Distortion Message Reader uses several lines of text from text.dat -(included with Synchronet in the sbbs/ctrl directory): -- Text # 10 (i.e., "E-mail (User name or number):"): Used when prompting the -user to confirm an email address/user number when sending a private reply to a -message -- Text # 30 (i.e., "Aborted."): Used when saving a reaply message was aborted -(i.e., because the user typed an empty email address) -- Text # 54 (i.e., "Delete mail from %s"): Used to confirm whether the user -wants to delete a personal email -- Text # 563 (i.e., "[Hit a key] "): Used for the screen pause when displaying -the help screen -- Text # 662 (i.e., "Download attached file: xyz.txt (500 bytes)"): Used to -confirm downloading an attached file - -When the user chooses to downloaded attached files, the reader will prompt to -confirm downloading. With Synchronet versions prior to 3.17, for the prompt -text, the reader will use text number 662 from text.dat (in the sbbs/ctrl -directory). If you want to customize the text for that confirmation prompt, -you would need to open text.dat and modify text number 662. - -4. Header ANSI/asc file -======================= -Digital Distortion Message Reader supports its own message header ANSI/.asc -file to be displayed above a message. This is separate from Synchronet's -msghdr file in the sbbs/text/menu directory. Digital Distortion Message -Reader's header file is read from the same directory as DDMsgReader.js and -needs to have the filename enhMsgHeader and can be in .ans or .asc format. -enhMsgHeader is short for "enhanced message header". If an ANSI-format (.ans) -version is found, it will be converted to a Synchronet .asc file using -Synchronet's ans2asc tool before being displayed. Also, you can create header -files for multiple terminal widths: To do so, the header filename format is -enhMsgHeader-width. For example, to create a header for a 132-column terminal, -the header filename would be enhMsgHeader-132.asc (or .ans). Digital -Distortion Message Reader will choose the one matching the user's terminal -width, if one exists. If no others are found, enhMsgHeader.asc (or .ans) will -be used, if it exists. If no enhMsgHdr file exists, Digital Distortion Message -Reader will use a default header (which adjusts to any terminal width). - -Many of Synchronet's @-codes (message variables) related to message information -are supported in the enhMsgHeader file. For a list of Synchronet's @-codes, -refer to the following web page: -http://www.synchro.net/docs/customization.html#MessageVariables -In addition, the @-codes can be displayed in fixed-width fields in the -enhMsgHeader file, using # characters in the @-code. For instance, to display -a message's subject left-justified in a width of 30 characters, the @-code -would look like this: -@MSG_SUBJECT-L###############@ - -There is also an additional @-code supported by Digital Distortion Message -Reader, MSG_NUM_AND_TOTAL (and MSG_NUM_AND_TOTAL-L), which will display both -the current message number and the total number of messages in the message -area. It can also be used in a fixed-width field. For example, to display it -left-justified in a width of 30 characters, the @-code would look like this: -@MSG_NUM_AND_TOTAL-L#########@ - -Another additional @-code supported by Digital Distortion Message Reader is -@MSG_FROM_AND_FROM_NET@, which shows the 'from' username along with the 'from' -network type in paranthesis. There is also a -L version for left- -justification with length. For instance: -@MSG_FROM_AND_FROM_NET-L######@ - - -5. User avatars -=============== -Digital Distortion Message Reader supports Synchronet's Avatar feature, which -was added in Synchronet 3.17. Basically, this feature allows users to have a -small (10x6) text-based artwork that represents themselves and is displayed in -the message header when reading messages. Digital Distortion Message Reader -has a setting in the configuration file, displayAvatars, which lets you toggle -whether or not to dislpay user avatars. Valid values are true and false. For -more information on Synchronet's Avatar feature, see the following wiki page -with a web browser: -http://wiki.synchro.net/module:avatars -For the avatars feature to work, ensure you have all the latest Synchronet .js -files in your sbbs/exec and sbbs/exec/load directories. Specifically, Digital -Distortion Message Reader loads smbdefs.js and avatar_lib.js, which are both -in the sbbs/exec/load directory. If those files are not there, then Digital -Distortion Message Reader will still work but it won't display avatars. -When avatars are enabled with Digital Distortion Message Reader, they will be -displayed in the message header above a message, on the right side of the -screen. If you create a custom message header file for Digital Distortion -Message Reader, you may want to reserve the rightmost 12 characters for the -user avatar. - - -6. Configuration file & color/text theme configuration file -=========================================================== -Digital Distortion Message Reader allows changing some settings, colors, and -some of the text via configuration files. - -Also, ddmr_cfg.js is a menu-driven configuration script to help with changing -configuration options. You can run it at a command prompt in the DDMsgReader -directory with the following command: -jsexec ddmr_cfg -Alternately (with the filename extension): -jsexec ddmr_cfg.js - -If you have DDMsgReader in the standard location (xtrn/DDMsgReader), ddmr_cfg -will copy the configuration file to your sbbs/mods directory to help prevent it -from being accidentally overridden by updating the standard Synchronet files. - -The configuration files are plain text and can be edited with any text editor. -These are the configuration files used by Digital Distortion Message Reader: -- DDMsgReader.cfg: The main configuration file -- DefaultTheme.cfg: Defines colors & some text strings used in the reader. - The name of this file can be specified in DDMsgReader.cfg, so that alternate - "theme" configuration files can be used if desired. - -Each setting in the configuration files has the format setting=value, where -"setting" is the name of the setting or color, and "value" is the corresponding -value to use. The colors specified in the theme configuration file are -Synchronet color/attribute codes. Comments are allowed in the configuration -files - Commented lines begin with a semicolon (;). - -Digital Distortion Message Reader will look for the configuration files in the -following directories, in the following order: -1. sbbs/mods -2. sbbs/ctrl -3. The same directory as DDMsgReader.js -If you customize your configuration files, you can copy them to your sbbs/mods -or sbbs/ctrl directory so that they'll be more difficutl to accidentally -override if you update your xtrn/DDMsgReader from the Synchronet CVS -repository, where this reader's files are checked in. - -The configuration settings are described in the sections below: - -Main configuration file (DDMsgReader.cfg) ------------------------------------------ -Setting Description -------- ----------- -listInterfaceStyle String: The user interface to use for message - lists. Valid values are Traditional (non- - lightbar user interface with user prompted for - input at the end of each screenful) or Lightbar - (use the lightbar user interface). - -reverseListOrder Default for the user setting for whether or - not to display message lists in reverse order. - Valid values are true or false. When a message - list is displayed in reverse, it will be listed - in descending order by date & time. - -readerInterfaceStyle The user interface style to use for the - message reader. Valid values are - Scrollable and Traditional. The - scrollable interface allows scrolling the - message up and down and is only available - for ANSI users. If a user is not using - ANSI, the reader will use the traditional - user interface instead, regardless of - this setting. - -readerInterfaceStyleForANSIMessages The user interface style to use for - reading messages with ANSI content. Valid - values are Scrollable and Traditional. - The scrollable interface allows scrolling - the message up and down. If false, the - reader will use a traditional (non- - scrolling) user interface to display - messages with ANSI content. If a user is - not using ANSI, the reader will use the - traditional user interface instead, - regardless of this setting. - -displayBoardInfoInHeader true/false: Whether or not to display sub-board - information above the column headers when listing - the message information. If this is set to true, - 2 extra lines on the screen will be used at the top - to display message group and sub-board. - -promptToContinueListingMessages true/false: Whether or not to prompt the - user to continue listing messages after - a message is read. - -promptConfirmReadMessage true/false: Whether or not to prompt the - user to read a message when one is - selected. - -msgListDisplayTime Specifies the message date to display. - Valid values are imported and written. - imported: Display the message import dates - written: Display the message written dates - When using the message written dates, the - message written dates will be adjusted to - the BBS's local time zone so that they are - all consistent. - -msgAreaList_lastImportedMsg_time In the message sub-board lists, the date - & time of the last message in the - sub-board. This setting specifies - whether to use the imported time or the - written time. Valid values are imported - and written. When using the message - written dates, the message written dates - will be adjusted to the BBS's local time - zone so that they are all consistent. - -startMode Specifies whether to start in list mode - or reader mode. Valid values are Reader - (or Read) and Lister (or List). Note - that this setting can be overridden by - the -startMode command-line argument - (valid values are list and read). - -tabSpaces The number of spaces to use for tabs in - the message reader (tabs will be replaced - by this many spaces). - -pauseAfterNewMsgScan Whether or not to pause (i.e., with a - "finished" message) after doing a new - message scan. Valid values are true - and false. - -readingPostOnSubBoardInsteadOfGoToNext For reading messages (not for a newscan, - etc.): Whether or not to ask the user - whether to post on the sub-board in reader - mode after reading the last message - instead of prompting to go to the next - sub-board. This is like the stock - Synchronet behavior. Valid values are true - and false. This defaults to false. - -areaChooserHdrFilenameBase The filename to use (without the - extension) for a header to display above - the message area chooser list. For - example, if areaChgHeader is specified, - then the reader will look for - areaChgHeader.ans if it exists, and if - not, the reader will look for - areaChgHeader.asc. Additionally, you - can have multiple header files for - different terminal widths; fpr example, - areaChgHeader-80.ans for an 80-column - terminal, areaChgHeader-140.ans for a - 140-column terminal, etc. - -areaChooserHdrMaxLines The maximum number of lines to use from - the message area chooser header file. - -displayAvatars Whether or not to display user avatars in - message headers. Valid values are true - and false. - -rightJustifyAvatars Whether or not to right-justify avatars. - Valid values are true and false. Flase - means to left-justify avatars. - -msgListSort How to sort the message list. This can - be either Received (to sort by date/time - received) or Written (to sort by - date/time written). Received is the - fastest, as it does not sort the list; - Written adds time since sorting is - required. - -convertYStyleMCIAttrsToSync Whether or not to convert Y-style MCI - attribute codes to Synchronet attribute - codes. Valid values are true and false. - -prependFowardMsgSubject Whether or not to prepend the subject for - forwarded messages with "Fwd: ". Valid - values are true and false. Defaulse to - true. - -enableIndexedModeMsgListCache For indexed reader mode, whether or not to - enable caching the message header lists - for performance - -quickUserValSetIndex The index of the quick-validation set to - use for quick-validating a local user. - Normally, this should be 0-9, as there are - 10 sets of values in SCFG). Alternately, - quickUserValSetIndex can be set to - something invalid (like -1) to have a menu - of the quick-validation sets displayed for - you to choose from one. - -saveAllHdrsWhenSavingMsgToBBSPC For the sysop, whether to save all message - headers when saving a message to the BBS - PC. This could be a boolean (true/false) - or the string "ask" to prompt every time - -useIndexedModeForNewscan Default for a user setting for whether or - not to use indexed mode for doing a - newscan. For newscan only (not new - to-you). If enabled, a newscan will appear - as a menu listing the various sub-boards - and how many total messages and number of - new messages they have. This is the - default for a user setting; users can - toggle this for themselves as they like. - -displayIndexedModeMenuIfNoNewMessages Default for a user setting for whether or - not to use the indexed menu for newscans - even when there are no new messages. - Valid values are true or false. - -newscanOnlyShowNewMsgs Default for a user setting: whether or not - to only show new messages during a - newscan. This can help speed up newscans - if your BBS has a lot of messages in the - sub-boards. Users can toggle this as they - like. - -indexedModeMenuSnapToFirstWithNew For the indexed newscan sub-board menu in - lightbar mode, whether or not to 'snap' - the selected item to the next sub-board - with new messages upon displaying or - returning to the indexed newscan sub-board - menu. This is a default for a user setting - that users can toggle for themselves. - - -promptDelPersonalEmailAfterReply Default for a user setting: When reading - personal email, whether or not to propmt - the user if they want to delete a message - after replying to it - -themeFilename The name of the configuration file to - use for colors & string settings - -Theme configuration file ------------------------- -The convention for the setting names in the theme configuration file is that -setting names ending in 'Text' are for whole text strings, and the setting -names that don't end in 'Text' are for colors. - -Note that if one of the strings has a space at the end, a : must be used -instead of a = to separate the setting name from the value. The value can also -be inside double-quotes. See the following for more information: -https://wiki.synchro.net/config:ini_files#string_literals - -Setting Description -------- ----------- -headerMsgGroupTextColor Color for the header line in the message - list displaying the message group, for the - text "Current msg group:" - -headerMsgGroupNameColor Color for the message list header line - displaying the message group - -headerSubBoardTextColor Color for the message list header line - displaying the message sub-board, for the - text "Current msg sub-board:" - -headerMsgSubBoardNameColor Color for the message list header line - displaying the message sub-board - -listColHeaderColor Color for the column names in the message - list - -msgListMsgNumColor Color for the message number (in the - message list) -msgListFromColor Color for the sender name (in the message - list) -msgListToColor Color for the destination name (in the - message list) -msgListSubjectColor Color for the message subject (in the - message list) -msgListScoreColor Color for the message score (in the - message list) - For terminals at least - 86 characters wide -msgListDateColor Color for the message date (in the message - list) -msgListTimeColor Color for the message time (in the message - list) - -msgListToUserMsgNumColor Color for the message numer, for messages - to the user (in the message list) -msgListToUserFromColor Color for the sender name, for messages to - the user (in the message list) -msgListToUserToColor Color for the destination name, for - messages to the user (in the message list) -msgListToUserSubjectColor Color for the message subject, for - messages to the user (in the message list) -msgListToUserScoreColor Color for the message score, for - messages to the user (in the message list) - - For terminals at least 86 characters - wide -msgListToUserDateColor Color for the message date, for messages - to the user (in the message list) -msgListToUserTimeColor Color for the message time, for messages - to the user (in the message list) - -msgListFromUserMsgNumColor Color for the message number, for messages - from the user (in the message list) -msgListFromUserFromColor Color for the mender name, for messages - from the user (in the message list) -msgListFromUserToColor Color for the mestination name, for - messages from the user (in the message - list) -msgListFromUserSubjectColor Color for the message subject, for - messages from the user (in the message - list) -msgListFromUserScoreColor Color for the message score, for - messages from the user (in the message - list) - For terminals at least 86 - characters wide -msgListFromUserDateColor Color for the message date, for messages - from the user (in the message list) -msgListFromUserTimeColor Color for the message time, for messages - from the user (in the message list) - -msgListHighlightBkgColor Message list highlighting background color - (for lightbar mode) -msgListMsgNumHighlightColor Message list highlighted message number - color (for lightbar mode) -msgListFromHighlightColor Message list highlighted 'from' name color - (for lightbar mode) -msgListToHighlightColor Message list highlighted 'to' name color - (for lightbar mode) -msgListSubjHighlightColor Message list highlighted subject color - (for lightbar mode) -msgListScoreHighlightColor Message list highlighted score color - (for lightbar mode) - For terminals at - least 86 characters wide -msgListDateHighlightColor Message list highlighted date color (for - lightbar mode) -msgListTimeHighlightColor Message list highlighted time color (for - lightbar mode) - - Colors for the indexed mode sub-board menu: -indexMenuHeader Header text above the indexed mode menu - -indexMenuNewIndicator "NEW" indicator text at the start of the - indexed mode menu sub-boards that have new - messages - -indexMenuDesc Indexed mode menu item description - -indexMenuTotalMsgs Indexed mode menu total number of messages - -indexMenuNumNewMsgs Indexed mode menu number of new messages - -indexMenuLastPostDate Indexed mode menu last post date - -indexMenuHighlightBkg Indexed mode menu highlighted item - background - -indexMenuNewIndicatorHighlight Indexed mode highlighted "NEW" indicator - text at the start of the sub-boards that - have new messages - -indexMenuDescHighlight Indexed mode menu highlighted description - -indexMenuTotalMsgsHighlight Indexed mode menu highlighted total number - of messages - -indexMenuNumNewMsgsHighlight Indexed mode menu highlighted number of new - messages - -indexMenuLastPostDateHighlight Indexed mode menu highlighted last post - date - -indexMenuSeparatorLine Indexed mode menu: Horizontal line - separating sub-boards - -indexMenuSeparatorText Indexed mode menu: Sub-board name - separating sub-boards - -Colors for the indexed mode lightbar help line text: -lightbarIndexedModeHelpLineBkgColor Indexed help line background - -lightbarIndexedModeHelpLineHotkeyColor Indexed help line hotkey color - -lightbarIndexedModeHelpLineGeneralColor Indexed help line general text color - -lightbarIndexedModeHelpLineParenColor Indexed help line - For the ) separating - the hotkeys from general text - -Lightbar message list help line colors: - -lightbarMsgListHelpLineBkgColor Background color for the lightbar message - list help line -lightbarMsgListHelpLineGeneralColor Color for the general text in the lightbar - message list help line -lightbarMsgListHelpLineHotkeyColor Color for the hotkeys in the lightbar - message list help line -lightbarMsgListHelpLineParenColor Color for the ) characters in the lightbar - message list help line - -tradInterfaceContPromptMainColor Color for the text displayed in the - 'continue' prompt in the traditional user - interface in the message list - -tradInterfaceContPromptHotkeyColor Color for the hotkeys displayed in the - 'continue' prompt in the traditional user - interface in the message list - -tradInterfaceContPromptUserInputColor Color for the user input at screen pauses - in the traditional user interface for the - message list - -msgBodyColor Color for the message body when reading a - message - -readMsgConfirmColor Color for the text used to confirm whether - to read a message - -readMsgConfirmNumberColor Color for the message number displayed - when asking the user if they really want - to read the message - -afterReadMsg_ListMorePromptColor Color for the text used for asking the user - if they want to continue listing messages - -tradInterfaceHelpScreenColor Color for the text used in the traditional - user interface message list help screen - -areaChooserMsgAreaNumColor Color for the message area numbers when - choosing a different message area - -areaChooserMsgAreaDescColor Color for the message area descriptions - when choosing a different message area - -areaChooserMsgAreaNumItemsColor Color for the number of items when - choosing a different message area - -areaChooserMsgAreaHeaderColor Color for the message area header line - when choosing a different message area. - This is the line that shows the - message group and page number. - -areaChooserSubBoardHeaderColor Color for the sub-board information line - when choosing a different message area - -areaChooserMsgAreaMarkColor Color for the mark character showing the - current message area when choosing a - different message area - -areaChooserMsgAreaLatestDateColor Color for the latest message date when - choosing a different message area - -areaChooserMsgAreaLatestTimeColor Color for the latest message date when - choosing a different message area - -- Colors for the built-in header displayed above a message (if not using your - own message header ANSI - -msgHdrMsgNumColor Color for the message number displayed in - the header above a message - -msgHdrFromColor Color for the 'From' name displayed in the - header above a message - -msgHdrToColor Color for the 'To' name displayed in the - header above a message - -msgHdrToUserColor Color for the 'To' name displayed in the - header above a message when the message is - written to the current user - -msgHdrSubjColor Color for the subject displayed in the - header above a message - -msgHdrDateColor Color for the message date displayed in - the header above a message - -- Highlighted versions of the above message area list colors: -areaChooserMsgAreaBkgHighlightColor -areaChooserMsgAreaNumHighlightColor -areaChooserMsgAreaDescHighlightColor -areaChooserMsgAreaDateHighlightColor -areaChooserMsgAreaTimeHighlightColor -areaChooserMsgAreaNumItemsHighlightColor - -lightbarAreaChooserHelpLineBkgColor Background color for the lightbar - message area chooser help line -lightbarAreaChooserHelpLineGeneralColor Color for the general text in the - lightbar message area chooser help - line -lightbarAreaChooserHelpLineHotkeyColor Color for the hotkeys in the lightbar - message area chooser help line -lightbarAreaChooserHelpLineParenColor Color for the ) characters in the - lightbar message area chooser help line - -scrollbarBGColor Color for the scrollbar background in the - scrollable message reader interface - -scrollbarBGChar The character to use for the scrollbar - background characters in the scrollable - message reader interface - -scrollbarScrollBlockColor Color for the moving scrollbar block in - the scrollable message reader interface - -scrollbarScrollBlockChar The character to use for the moving - scrollbar block characters in the - scrollable message reader interface - -enhReaderPromptSepLineColor Color for the line drawn in the 2nd to - last line of the message area in the - scrollable message reader interface - before a prompt is displayed on the next - line - -goToPrevMsgAreaPromptText Text to use for prompting the user whether - or not to go to the previous message area - (without the ? on the end) - -goToNextMsgAreaPromptText Text to use for prompting the user whether - or not to go to the next message area - (without the ? on the end) - -enhReaderHelpLineBkgColor Color to use for the background of the - hotkey help line displayed at the bottom - of the scrollable message reader interface - -enhReaderHelpLineGeneralColor Color to use for general text in the - hotkey help line displayed at the bottom - of the scrollable message reader interface - -enhReaderHelpLineHotkeyColor Color to use for hotkeys in the hotkey - help line displayed at the bottom of the - scrollable message reader interface - -enhReaderHelpLineParenColor Color to use for ) characters in the - hotkey help line displayed at the bottom - of the scrollable message reader interface - -postOnSubBoard The text to use for asking the user whether - they want to post on a sub-board (for - instance, after reading the last message). - The two %s will be replaced with the - message group name and sub-board - description, respectively. - -newMsgScanText The first text displayed when doing a new - message scan, before the sub-board/group/ - all prompt is displayed - -newToYouMsgScanText The first text displayed when doing a - new-to-you message scan, before the - sub-board/group/all prompt is displayed - -allToYouMsgScanText The first text displayed when doing a - all-messages-to-you message scan, before - the sub-board/group/all prompt is - displayed - -msgScanCompleteText Text to display when the message scan is - complete - -msgScanAbortedText Text to display when the message scan has - been aborted - -msgSearchAbortedText Text to display when the message search has - been aborted - -searchingSubBoardAbovePromptText Text for "Searching message sub-board: .." - above the search text prompt (%s is - replaced with the sub-board name) - -searchingSubBoardText For displaying the sub-board name when - doing a search. %s will be replaced with - a sub-board name. - -scanningSubBoardText For displaying the sub-board name when - doing a scan (i.e., a newscan). %s will be - replaced with a sub-board name. - -noMessagesInSubBoardText No messages in a sub-board (i.e., trying - to read a sub-board that has no messages). - %s will be replaced with a sub-board name. - -noSearchResultsInSubBoardText For no search results found in a - sub-board. %s will be replaced with a - sub-board name. - -readMsgNumPromptText Prompt text to input a number of a message - to read - -invalidMsgNumText Text to display for an invalid message - number. %d will be replaced with the - message number. - -msgHasBeenDeletedText Text to display when a message has been - deleted. %d will be replaced with the - message number. - -noKludgeLinesForThisMsgText Text to display when the user tries to - display kludge lines but there are no - kludge lines in the message - -searchingPersonalMailText Text for "Seraching personal mail" - -searchTextPromptText Text for prompting for search text - -fromNamePromptText Text for prompting for a 'from' name - -toNamePromptText Text for prompting for a 'To' name - -abortedText Text for an aborted operation (i.e., - "Aborted") - -loadingPersonalMailText Text for the status message "Loading - personal mail..." %s will be replaced - with "Personal mail". - -msgDelConfirmText Prompt text to confirm message deletion - (without the ? at the end). %d will be - replaced with the message number. - -delSelectedMsgsConfirmText Prompt text to confirm deletion of - selected messages (i..e, for batch message - deletion), without the ? at the end. - -msgDeletedText Text for when a message has been marked - for deletion. %d will be replaced with - the message number. - -selectedMsgsDeletedText Text for when selected messages have been - marked for deletion - -cannotDeleteMsgText_notYoursNotASysop Error text for cannot delete a message - because the message is not the user's - message or the user is not a sysop. %d - will be replaced with the message - number. - -cannotDeleteMsgText_notLastPostedMsg Error text for cannot delete a message - because it's not the user's last posted - message in a sub-board. %d will be - replaced with the message number. - -msgEditConfirmText Prompt text to confirm message edit - (without the ? at the end). %d will be - replaced with the message number. - -noPersonalEmailText The text to output when the user has no - personal email messages (i.e., "You have - no messages.") - -deleteMsgNumPromptText The text to output when prompting for the - number of a message to delete - -editMsgNumPromptText The text to output when prompting for the - number of a message to edit - -hdrLineLabelColor The color to use for the header/kludge - line labels - -hdrLineValueColor The color to use for the header/kludge - line values - -selectedMsgMarkColor The color to use for the checkmark for - selected messages (used in the message - list) - -unreadMsgMarkColor The color to use for the 'unread' message - marker character in the message list - (appears as a U) - -7. Indexed reader mode -====================== -"Indexed" reader mode is a new mode that was added to DDMsgReader v1.70. This -mode displays a menu/list of message sub-boards within their groups with total -and number of new messages and allows the user to select one to read. There is -also a user setting to allow the user to use this mode for a newscan (rather -than the traditional newscan) if they wish. - -Indexed reader mode may also be started with the -indexedMode command-line -parameter. For example, if you are using a JavaScript command shell: - bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode"); -With the above command-line parameter, DDMsgReader will show all sub-boards the -user is allowed to read and which they have in their newscan configuration. -If the user has enabled indexed mode for newscans, then during a newscan, it -will show sub-boards based on the user's chosen option for current -sub-board/group/all. - -This is an example of the sub-board menu that appears in indexed mode - And from -here, the user can choose a sub-board to read: - -Description Total New Last Post -─────────────────────────────────────────────────────────────────────────────── - AGN GEN - General Chat 1004 0 2023-04-02 - AGN BBS - BBS Discussion 1000 0 2023-01-17 -NEW AGN ART - Art/Demo Scene 603 1 2023-04-02 - AGN DEV - Software Development 398 0 2021-11-09 - AGN NIX - Unix/Linux Related 297 0 2023-04-02 - AGN L46 - League Scores & Recons 1000 0 2016-09-10 -NEW AGN TST - Testing Setups 2086 10 2023-04-03 - AGN SYS - Sysops Only 1000 0 2023-01-19 -───── FIDO - FidoNet ────────────────────────────────────────────────────────── -NEW BBS CARNIVAL - BBS Software Chatter 660 5 2023-04-04 - BBS INTERNET - DOS/Win/OS2/Unix Internet BBS Applicatio 18 0 2023-03-04 - CHWARE - Cheepware Support/Discussion 111 0 2023-03-16 - CLASSIC COMPUTER - Classic Computers 191 0 2023-02-10 - CONSPRCY - Conspiracy Discussions 59 0 2023-03-14 - CONTROVERSIAL - Controversial Topics, current events, at 3 0 2023-01-31 -NEW DOORGAMES - BBS Doorgames and discussions 288 1 2023-04-03 -NEW FUNNY - FUNNY Jokes and Stories 1184 3 2023-04-04 - FUTURE4FIDO - Discussion of new and future Fidonet tec 152 0 2023-04-01 - LINUX BBS - Linux BBSing 46 0 2023-04-01 - LINUX - Linux operating system (OS), a Unix vari 1076 0 2023-04-01 - LINUX-UBUNTU - The Ubuntu Linux Distribution Discussion 18 0 2023-02-17 -NEW MEMORIES - NOSTALGIA 2434 3 2023-04-03 - - -8. Quick-Validating users (while reading their message) -======================================================= -While reading messages, the sysop may apply quick-validation values to a local -user using the Ctrl-Q hotkey. Quick-Validation sets are configured in SCFG > -System > Security Options > Quick-Validation Values. In DDMsgReader.cfg, the -option quickUserValSetIndex can be used to set the index of the quick-validation -set to use (normally it would be 0-9, as there are 10 sets of values in SCFG). -Alternately, quickUserValSetIndex can be set to something invalid (like -1) for -DDMsgReader to display a menu of the quick-validation sets to let you choose -one. - -DDMsgReader applies the flag sets, exemptions, and restrictions to a user in -addition to any that the user might already have, so that any that you have -added for a user will be preserved (DDMsgReader does a bitwise 'or'). - -A quick-validation set in CFG is a set that includes a security level, flag -sets, exemptions, restrictions, and additional credits. For example: -╔[■][?]═══════════════════╗ -║ Quick-Validation Values ║ -╠═════════════════════════╣ -║ ?0 SL: 5 F1: ║ -║ ?1 SL: 10 F1: ║ -║ ?2 SL: 20 F1: ║ -║ ?3 SL: 30 F1: ║ -║ ?4 SL: 40 F1: ║ -║ ?5 SL: 50 F1: ║ -║ ?6 SL: 60 F1: ║ -║ ?7 SL: 70 F1: ║ -║ ?8 SL: 80 F1: ║ -║ ?9 SL: 90 F1: ║ -╚═════════════════════════╝ - - -9. Drop file for replying to messages with Synchronet message editors -===================================================================== -When reading a message, the message lister will write a drop file in the node -directory, called DDML_SyncSMBInfo.txt, which contains some information about -the message being read, for use by Synchronet message editors. This drop file -is provided for Synchronet message editors to get information about the current -message being read if needed. In Synchronet's JavaScript object model, there -are certain variables that are provided about the current message and sub-board -being read that are normally updated by Synchronet's built-in message read -prompt, but JavaScript scripts cannot modify those values. Because of that, -the solution was for the message lister to provide the message information in a -drop file in the node directory just before allowing the user to reply to a -message. When the user is done replying to the message, DDML_SyncSMBInfo.txt -will be deleted. -DDML_SyncSMBInfo.txt contains the following information: -Line 1: The highest message number in the sub-board. Equivalent to - bbs.smb_last_msg in Synchronet's JavaScript object model. -Line 2: The total number of messages in the sub-board. Equivalent to - bbs.smb_total_msgs in Synchronet's JavaScript object model. -Line 3: The (absolute) message number of the message being read. Equivalent to - bbs.msg_number in Synchronet's JavaScript object model and the "header" - property of a message header. -Line 4: The sub-board code (text). Equivalent to bbs.smb_sub_code in - Synchronet's JavaScript object model. - -One message editor in particular, SlyEdit, needs to be able to determine which -message is currently being read and replied to so that it can retrieve the -message author's initials for use in quoting the messsage. Normally, SlyEdit -will get the message information from the aforementioned JavaScript variables -provided by Synchronet, but if DDML_SyncSMBInfo.txt exists, SlyEdit will read -the message information from that file instead. -Note that if you have SlyEdit installed on your BBS, this version of Digital -Distortion Message Reader (1.00) requires version 1.27 or newer of SlyEdit in -order for SlyEdit to properly get the correct message from the message lister. - -10. text.dat lines used in Digital Distortion Message Reader -=========================================================== -This message reader uses the following lines from Synchronet's text.dat file -(located in the sbbs/ctrl directory): -10 (Email) -30 (Aborted) -54 (DeleteMailQ) -117 (MessageScanComplete) -390 (UnknownUser) -501 (SelectItemHdr) -503 (SelectItemWhich) -563 (Pause) -578 (QWKNoNewMessages) -621 (SubGroupOrAll) -662 (DownloadAttachedFileQ) -759 (CantReadSub) -779 (VotingNotAllowed) -780 (VotedAlready) -781 (R_Voting) -783 (VoteMsgUpDownOrQuit) -787 (PollVoteNotice) diff --git a/revision_history.txt b/revision_history.txt deleted file mode 100644 index d432b8fbb3d40c20ae467fc1f953b7511e69dc53..0000000000000000000000000000000000000000 --- a/revision_history.txt +++ /dev/null @@ -1,581 +0,0 @@ -This file lists all of the changes made for each release of the Digital -Distortion Message Reader. - -Revision History (change log) -============================= -Version Date Description -------- ---- ----------- -1.93a 2024-01-07 Fix: For indexed read mode (not doing a newscan), when - choosing a sub-board to read, the correct (first unread) - message is displayed. Also, the user's scan pointer is - also updated to the last_read pointer. - New operator option for read mode: Add author email to email.can -1.93 2024-01-01 New user-toggleable behavior: Show indexed menu after - reading all new messages - Also, indexed reader mode (started with the -indexedMode - command-line option) now lists ALL sub-boards, rather - than only sub-boards the user has enabled for newscan. - It also prompts the user to list sub-boards in the current - group or all. -1.92 2023-12-29 Indexed newscan: By default, if there are no new messages, - it now shows "No new messages." (578 QWKNoNewMessages from - text.dat). There's a new user setting to toggle whether to - use the indexed newscan menu even if there are no new - messages. - New configuration file option: - displayIndexedModeMenuIfNoNewMessages, which is a default - for a user setting to toggle whether or not to use the - Indexed newscan menu even when there are no new messages. -1.91 2023-12-26 New sysop features while reading a message: Show message - hex (with the X key) and save message hex to a file (with - Ctrl-X) -1.90b 2023-12-15 New configurable colors in the theme file for the indexed - newscan menu header text (indexMenuHeader), "NEW" - indicator text (indexMenuNewIndicator), and highlighted - "NEW" indicator text (indexMenuNewIndicatorHighlight) -1.90a 2023-12-12 New configurable colors in the theme file for the indexed - mode newscan menu: indexMenuSeparatorLine (sub-board - separator line) and indexMenuSeparatorText (sub-board - separator text) -1.90 2023-12-04 New: operator menu for read mode, with the option to add - the author to the twit list, etc. - Fix: When refreshing a rectangular area of a message, if - it's a poll message, the background color for the voted - responses was used for the non-selected responses. - Removed the setting useScrollingInterfaceForANSIMessages. -1.89 2023-11-30 New: User option to toggle whether to display the email - 'replied' indicator (defaults to true). - Fix for setting colors for the key help lines so that the - background won't get un-done if the other help line colors - have a N (normal) attribute. -1.88 2023-11-24 New user setting/configuration option to prompt the user - whether or not to delete a personal email after replying - to it (defaults to false). - New: Displays whether a personal email has been replied - to. - Fix: Now displaying message vote score in the default - header again. - Fix: When viewing message headers (for the sysop), now - correctly shows the message attributes. -1.87 2023-11-18 Possible speed improvement when loading messages. - New: User setting to only show new messages in a newscan - (defaults to true/enabled) - In the message list, there is now an additional space - before the 'from' name, in case one of the status - characters is a letter (this should look better). - New: In lightbar mode, the indexed newscan menu can - optionally 'snap' to the next sub-board with new messages - when showing/returning to the menu - Fix: When listing personal email, messages to the user - were written with the to-user color wuen unread. Now the - regular colors are always used (since all of a user's - personal emails are 'to' them). - Fix: For indexed newscan, if there are no sub-boards - selected for scan in the user's newscan configuration, - then output a message and exit. Otherwise, it would end - up in an infinite loop. - Updated how user settings are loaded, to ensure that - default user settings from DDMsgReader.cfg actually get - set properly in the user settings. -1.86 2023-11-09 New feature: For indexed mode, when choosing a sub-board, - the R key can be used to mark all messages as read in the - sub-board. - Fix: For continuous newscan or browse newscan (SCAN_BACK), - call the stock Synchronet behavior (DDMsgReader did this - previously, as DDMsgReader doesn't implement those yet). - Fix: In the message list, to-user alternate colors weren't - being used unless the message was read. The correct colors - are used again. -1.85 2023-11-01 Mark personal email as read if the user is just reading - personal email -1.84 2023-10-26 Fix in reader mode for refreshing the message area after - closing another window (necessary with recent changes to - substrWithAttrCodes()) -1.83 2023-10-25 Personal emails to the sysop received as "sysop" (or - starting with "sysop") are now correctly identified and - marked as read when read -1.82 2023-10-18 Fix for # posts and missing dates in sub-board list when - changing sub-board -1.81 2023-10-11 Updated permission check functions (speed improvement) -1.80 2023-10-10 Improved speed of new-to-you scans, and to an extent - (hopefully) overall speed - Bug fix: Setting reverseListOrder to "ask" in the .cfg - file works properly again. - Bug fix: When listing messages in reverse order, the - selected menu index (for lightbar mode) is now correct. - Bug fix: If the user is allowed to read deleted messages, - then allow the left & right arrow keys to to the next or - previous message if it's deleted. - Small fixes for indexed scanning mode. - New: For personal email, unread emails will have an - 'unread' message indicator in the message list as a U - between the message number and the 'from' name. - New user setting: "Quit from reader to message list": - When enabled, quitting from reader mode goes to the - message list instead of exiting out of DDMsgReader fully. - New user setting: Enter/selection from indexed mode menu - shows message list (instead of going into reader mode) - New user setting: List messages in reverse order -1.79 2023-09-20 Fixed poll voting for single-answer polls -1.78 2023-08-30 Bug fix for going to a specific message in the message - list (especially for lightbar mode) -1.77a 2023-08-26 When saving a message on the local BBS PC without all the - headers, the date is now included -1.77 2023-08-20 Including all message headers when saving a message (sysop - only) is now optional. -1.76 2023-08-18 Fix for "Message header has 'expanded fields'" error when - updating message header attributes in certain conditions -1.75 2023-08-16 Made some changes to allow easy searching of personal - email with command-line arguments. -1.74 2023-04-29 Settings for users being able to read deleted messages now - applies to personal email. Also, allows reading messages - that are marked for deletion in addition to just seeing - them in the message list -1.73a 2023-04-25 For viewing message headers, now all message header - information is displayed & sorted alphabetically by field - name (same with saving a message to a file). -1.73 2023-04-17 Bug fix: When getting header lines to view, ensure the - header lines are not too wide for the user's terminal. - Header lines that are too long will be split into no more - than 2 lines. -1.72 2023-04-16 Added a quick-validation hotkey, Ctrl-Q, for sysops to - use to apply a quick-validation set to a user when - reading their message. Quick-Validation sets are - configured in SCFG > System > Security Options > - Quick-Validation Values. -1.71 2023-04-07 Ctrl-C is now supported for message searches to abort the - search. A new configurable string was added for this - situation: msgSearchAbortedText -1.70 2023-04-04 Added "indexed" reader mode, which lists sub-boards with - total and number of new/unread messages and lets the user - read messages in those sub-boards. Also, utf-8 characters - should now be converted properfly for non utf-8 terminals. -1.69 2023-03-24 Bug fix for deleting multiple selected messages: When - updating message headers in the cached arrays, don't try - to save them back to the database, because that was - already done (this avoids a 'header has expanded fields' - error). -1.68 2023-03-15 Makes use of console.aborted when displaying help screens - so that screen updates work better after pausing output. - Also, when running a new message scan (not new-to-you), - the current sub-board being scanned is now outputted. - There is a new configurable text string: - scanningSubBoardText -1.67 2023-03-09 Fixes for time zone alignment & list key help for wide - terminals -1.66 2023-03-02 When forwarding a message, the subject can now be edited - before sending the message -1.65 2023-02-24 Ctrl-C can now be used to cancel message scans. Output - from the scan is now word-wrapped to the terminal width. -1.64 2023-02-09 When reading personal email (received or sent), as a - loadable module, now it can allow reading another user's - mail (for the sysop when deleting a user account). -1.63 2023-02-01 Fix for reading colors from the theme file. Also, the - theme file now no longer needs the control character for - color codes. -1.62 2023-01-30 (Hopefully) Improved display of ANSI messages which would - previously look bad with empty lines evrey other line -1.61 2023-01-22 Fix: When replying to an email with an unknown sender - (empty or "All"), no longer gives the error "Invalid user - field: 0"; also, if the sender is unknown, prompts the - user for a user name/number/email address to send the - reply to. -1.60 2023-01-20 DDMsgReader can now optionally convert Y-style MCI - attribute codes to to Synchronet attribute codes, with - the new configuration setting convertYStyleMCIAttrsToSync - (true/false). -1.59 2022-12-29 For Synchronet above 3.20, now reads the external editor - quote wrap setting from xtrn.ini. Below version 3.20, the - quote wrap setting is read from xtrn.cnf. - Also, there's a new user setting to toggle whether or not - to use the scrollbar in the scrolling reader. Currently - there is no alternate progress displayed if not using the - scrollbar, but that is planned for a future update. -1.58 2022-12-14 Now wraps quote lines, if applicable, according to the - quote line wrap settings of the user's external editor, - if the user uses one -1.57.1 2022-12-12 Fix for "assignment to undeclared variable" error -1.57 2022-12-02 @-codes were only expanded when reading personal mail; - now, DDMsgReader also checks to make sure the sender is a - sysop. -1.56 2022-11-25 Fixed bug startup mode for scanning all groups for un-read - messages to you where the reader was bringing up personal - email instead. -1.55 2022-09-23 Refactored how email replies are done (passing the header - to the appropriate functions, not using ungetstr() when - prompting for the message subject) -1.54 2022-08-06 Users now have a personal twit list, configurable via - user settings, with the Ctrl-U hotkey. -1.53 2022-07-18 Deleted messages can now be un-marked for deletion from - the message list (if the user has delete permissions). - Also, the reader now honors the system setting for whether - users can view deleted messages. -1.52 2022-07-09 Mouse click support for the bottom help lines in scrolling - mode (thanks to help from Nelgin) -1.51 2022-07-05 Graphic is now only used when using the scrollable - interface. Also, when creating the Graphic, now - subtracting 1 from the reading area height to avoid making - the Graphic one line too tall to avoid unnecessary - scrolling. - When saving messages with ANSI codes, Graphic is only used - if the message has any ASCII drawing characters. (not sure - if this really matters much though). - Also, applied "use strict" and made some changes as necessary. -1.50 2022-06-20 When doing a text search, it now ignores the user scan - configuration for sub-boards, to ensure it will show any - results of the text search. -1.49 2022-06-13 Refactor: Simplified saving a message to BBS machine for - sysop (as-is, less processing); removed attachment stuff - for pre-Synchronet 3.17; moved hasSyncAttrCodes() to - attr_conv.js because that's where it needs to be. -1.48 2022-06-12 Improved display of ANSI messages -1.47a 2022-03-23 Internal change: Now calls bbs.edit_msg() for editing an - existing message (for Synchronet 3.18 and up). - Functionally no change. -1.47 2022-03-14 DDMsgReader can now be called directly as a loadable - module by Synchronet (though requires the included - ddmr_lm.js if DDMsgReader.js is not in sbbs/exec or - sbbs/mods) -1.46 2022-03-07 Fix: When changing to an empty sub-board from within the - reader (either from read mode or list mode), it now - properly says there are no messages and exits, rather than - showing a list of bogus messages. Unsure when this bug - was introduced. -1.45d 2022-02-26 Fix for no group information available when displaying the - sub-board header above the message list when listing - personal email -1.45c 2022-02-25 Fixed score display and related colors in the message list - for wide terminals -1.45b 2022-02-25 Fixed message list time colors for wide terminals (above - 80 columns) -1.45 2022-02-24 Fixed message scanning & searching issue introduced in the - previous version. -1.44 2022-02-19 Removed the scanScopePromptText text line and used the - SubGroupOrAll line (621) from text.dat instead. Also, the - reader now supports @-code expansion in configured text - strings. - Text search now can search sub-board, group, or all like - the other text searching. - When reading the theme file, color settings are now - checked to ensure they only have Synchronet attribute - codes. -1.43 2022-02-10 Fixed the memory error when viewing message header info. - Also, when viewing message header information, it will no - longer show JS functions. -1.42 2022-01-13 Fixed attachment downloading. - Also, the first attempt at converting HTML entities in - HTML-formatted messages. - Also, added the ability to sort the message list by date - & time written rather than the import date/time. This is - specified in the configuration file via the msgListSort - option. -1.41 2021-02-12 Bug fix: When changing to another area with the lightbar - interface, if the user's current sub-board is a - high-numbered sub-board and they select a message group - with fewer sub-boards, the highlighted sub-board in that - group would be set to that high number and would be - incorrect. That has been fixed. Copied a fix from my - stand-alone message area chooser. In that scenario, the - current highlighted sub-board in the other group will be - the first one. -1.40 2021-01-31 (Michael Long) Fixed left/right colors not being - customizable on message list lightbar -1.39 2020-12-01 When forwarding a message, added the ability to optinally - edit the message before forwarding it. -1.38 2020-11-26 Bug fix: When forwarding a message, it now correctly sets - sets the to_net_type property in the message header to - FidoNet or internet for those types of message - destinations -1.37 2020-07-11 Added mouse support to the scrollable reader interface. - The integrated area changer functionality doesn't have - mouse support yet. -1.36 2020-05-23 Added a command-line parameter, -onlyNewPersonalEmail, - which specifies to list/read only new/unread personal - email to the user. And for integration with Synchronet - via the "Read Email" loadable module, this is to be used - together with the updated DDReadPersonalEmail.js. This - is intended to support the "read your unread mail only" - option in the email menu. -1.35 2020-05-13 Fixed some logic in determining how to address a personal - email when replying (either to a local user or via their - network address). -1.34 2020-05-11 The message list mode now honors anonymous posts, showing - the 'from' name as "Anonymous" (for non-sysops). The - sysop can still see the real name of the poster. The - reader mode already honored the 'anonymous' flag. -1.33 2020-04-21 Fixed: A new user starting to read messages in a sub-board - no longer causes an error (it checks for the scan_ptr - being 0xffffffff). This had been fixed in a couple places - previously, but apparently not this particular case. -1.32 2020-04-19 Removed some code that's no longer used. Also, fixed an - issue when changing to another sub-board with the - traditional-style (non-lightbar) list where it was slow to - list sub-boards. For the number of messages, it was - checking all headers to ignore ones marked as deleted, - etc., but that can be fairly slow.. Now it just uses - total_msgs for the MessageBase object, which is a lot - faster and still gives an idea of how many messages are - there. -1.31 2020-04-13 The area change feature now uses DDLightbarMenu. There - is no more internal lightbar code in this message reader. -1.30 2020-04-07 The message list features now uses DDLightbarMenu rather - than the internal lightbar menu code. Requires the - latest dd_lightbar_menu.js (in sbbs/exec/load). - Later I also plan to update the area chooser code to also - use DDLightbarMenu and remove the internal lightbar - chooser code from DDMsgReader altogether. -1.29 2020-04-03 When reading a message, if a message is written to the - current user, the 'To' username in the header above the - message is now written in a different color. Also, there - are new color settings available in the theme - configuration file (see the readme for descriptions): - msgHdrMsgNumColor, msgHdrFromColor, msgHdrToColor, - msgHdrToUserColor, msgHdrSubjColor, msgHdrDateColor -1.28 2019-12-21 Bug fix: When the user changes to a different message - area while reading a message, the reader would exit with - an error due to an invalid last-read message number. - This has been fixed. -1.27 2019-09-16 Bug fix: Now displays the message score in the header - even if the message only has downvotes -1.26 2019-09-12 Fixed a bug that was causing some of the message vote - tally information to be displayed as "undefined" -1.25 2019-08-29 Added the ability to search for message groups and - sub-boards when changing to another sub-board. Search - can be started with the / key or CTRL-F (Find). Also, in - lightbar mode, the N key can be used to highlight the - next match in the list. -1.24 2019-08-17 When making a private reply on local email, an error is - now outputted if the recipient's user number is not - found. Also, fixed an 'undefined' bug that happened when - searching for messages sometimes. -1.23 2019-07-27 If a message is in UTF-8 format and the user's terminal - doesn't support UTF-8, the message text will be converted - to CP437. Also, if there is a color/attribute code in - the message before the message text and there are no - other color/attribute codes, the color/attribute codes - will be removed so that the entire message isn't colored -1.22 2019-05-12 If the message score is 0, with upvotes and total_votes - both 0, then don't show the score when using the default - header ANSI. This is what was intended, but the 0 score - started showing up in more recent builds of Synchronet. -1.21 2019-05-04 New uses require() instead of load(), if the require() - function exists, to load required .js library scripts. - This helps avoid 'multiple definition' errors. The - require() function was added in Synchronet 3.17, so - if the require() function doesn't exist, then the reader - will use load(). -1.20 2019-04-26 Added configurable options for the message score colors - for the message list: msgListScoreColor, - msgListToUserScoreColor, msgListFromUserScoreColor, and - msgListScoreHighlightColor -1.19 2019-04-25 If the terminal size is wide enough (at least 86 - characters), the overall vote scores for the messages - is now displayed in the message list. Also, fixed a - bug introduced in the previous version where the vote - scores were no longer being displayed when reading a - message. It's no longer using MsgBase.get_index() and - uses get_all_msg_headers(), as before, since that's what - is required for message tallies to be included in the - message headers. -1.18 2019-04-15 Made use of the new MsgBase.get_index() function (if - available) for better performance. Added 'undefined' - checks for some of the messaeg attribute definitions - before adding them to the attribute strings, since some - of them have changed. -1.17 2019-01-02 Added support for Synchronet's new voting feature that - was added in Synchronet 1.17. Added support to allow the - sysop to validate messages in moderated message areas. - Fixed various bugs related to doing a newscan, displaying - messages with ANSI content, out-of-bounds error when - deleting a message, etc. Updated to set the message - number field length dynamically based on the number of - messages in the sub-board. It will be at least 4 but - can be more than 4 if there are 10000 messages or more - in a sub-board. Updated so that when listing personal - email, it will use the regular formatting colors rather - than the colors for messages to the user, since all - personal emails are to the user (the 'to user' colors for - each email might be obnoxious). - Also, updated to support Synchronet's Avatar feature - which was added in version 3.17. For that feature to - work, you will need the latest .js files - Specifically, - for this reader, you would need smbdefs.js and - avatar_lib.js. Added a new configuration setting, - displayAvatars, which toggles whether or not to display - user avatars in the message headers. Valid values are - true and false. - Also Added the new @-code MSG_FROM_AND_FROM_NET and - MSG_FROM_AND_FROM_NET-L (for left-justification with - field length), which shows the 'from' name with the from - network in parenthesis. Updated the default message - header to show that information. - Also contains various bug fixes. -1.16 2016-09-11 Added a new feature that allows users to forward a - message to an email address or to another user on the - BBS (using the O key). This can be useful, for - instance, if the user wants to send a message in a - public sub-board to their personal email for future - reference or send a message from a public sub-board to - another user to discuss the topic privately. -1.15 2016-08-29 - New user edit feature for sysops only: While reading - a message, the U key will edit the user account of the - author of the message (only if it's a local user account - on the BBS) - - Bug fix: Private reply on a networked sub-board - was no longer working (different from the bug fixed - in 1.14) - - Implemented a check to (hopefully) prevent a crash - related to parsing and replacing @-codes related to - file areas -1.14 2016-08-17 Bug fix: Version 1.13 was failing to reply to - private emails -1.13 2016-08-16 - Bug fix: Message number error when a new user starts - reading messages. - - Bug fix: Now, it should always successfully save a - message header with the READ attribute when the user - it was addressed to has read the message. This should - fix an issue where the same message would keep coming - up in a newscan, etc. -1.12 2016-05-11 - Updated the way the pause prompt is shown in the help - screen, in case the sysop has configured an external - module (Baja/JS) to run for a pause prompt. - - Potential bug fix: When translating a message number to - a message index, added a check to ensure the value is - a number, to (hopefully) avoid a potential crash. -1.11 2016-03-25 The reader now updates the number of new posts read by - the user during the session. This is represented by - bbs.posts_read in JavaScript. Also, did some internal - refactoring of the code, removing some old code leftover - from my message lister that is no longer needed in this - reader. DDMsgReader.js is a bit smaller due to the - refactor. -1.10 2016-02-19 Added a new configuration option, - readingPostOnSubBoardInsteadOfGoToNext, that affects what - happens after the user reads the last message on a - sub-board (for normal reading, not for newscans etc.): If - this is set to true, then the reader will prompt the user - if they want to post on the sub-board, then exit (this - is the stock Synchronet behavior). If this is set to - false, then the reader will prompt the user whether to go - to the next sub-board after reading the last message on a - sub-board. Also, added the postOnSubBoard text - configuration parameter for the theme filename, which - specifies the text to use for prompting the user if they - want to post on the sub-board after reading the last - message. - Added new configuration options areaChooserHdrFilenameBase - and areaChooserHdrMaxLines. These options specify the - filename base for a header file to use for the message - area chooser list and the maximum number of lines to use - from the area chooser header file. The filaname is - without the extension - The reader will first look for an - .ans version, then an .asc version. Additionally, - multiple header files can be used for different terminal - widths - For example, chooserMsgHdr-80.ans for an - 80-column terminal, choosrMsgHdr-140.ans for a 140-column - terminal, etc. - Updated so that when using the message written dates - (instead of the imported dates) in the message list & area - chooser, it will adjust the message written dates to the - BBS's local time zone so that they are all consistent. -1.09 2016-01-15 Updated to not center the message header lines - horizontally. Now, it will display the header lines - starting on column 1. This was done to fix a display - issue in some terminal software. -1.08 2016-01-10 Bug fix: When scanning message sub-boards, it wasn't - always closing the sub-board when there were no new - messages, resulting in further sub-boards failing to open - after a while. That has been fixed. -1.07 2015-12-24 - Added the ability to select multiple messages (for - actions such as batch delete), and added the ability to - delete multiple selected messages. Batch deleting is - only allowed when the user has permission to delete - messages (such as their own personal email). Messages - can be selected in the following ways: - o Lightbar message list: The spacebar selects an - individual message. CTRL-A lets the user select or - un-select all messages. - o Traditional message list: The S key lets the user - select or un-select messages, by typing message - numbers, A to select all, or N to select none - (un-select all). The list of message numbers is - comma-separated or space-separated, allowing - for number ranges such as 120-130 for instance. - o Reader interface: The spacebar selects the message. - To delete the selected messages, the user must be in the - message list; the CTRL-D key combo is used for batch - delete, and it will prompt the user for confirmation - before deleting the messages. - - Added the following configurable items in the theme - file: - delSelectedMsgsConfirmText - selectedMsgsDeletedText - cannotDeleteAllSelectedMsgsText - selectedMsgMarkColor -1.06 2015-12-13 - Updated so that a sub-board new-message-scan (with the - new_msg_scan_cur_sub command-line parameter) 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. - - Added a new configuration option, pauseAfterNewMsgScan, - which specifies whether or not to pause (i.e., with a - "finished" message) after doing a new message scan. - - Bug fix: The configFilename command-line parameter was - not being read correctly on startup; this has been fixed. - - Bug fix: Doing a new-message-scan should now always - display the correct sub-board @-code information in the - header above the message. -1.05 2015-12-06 - Improved displaying of messages with ANSI codes. The - reader now makes use of frame.js and scrollbar.js (in the - sync/exec/load directory) to enable a scrollable user - interface when displaying messages with ANSI content. - There is also a new configuration option, - readerInterfaceStyleForANSIMessages, which lets the sysop - configure whether to use a scrollable or traditional user - interface for ANSI messages. The reason for that option - is in case ANSI messages don't look good when using - frame.js & scrollbar.js - When set to a traditional user - interface for ANSI messages, the reader will use a - non-scrolling user interface for displaying messages with - ANSI codes, which simply sends the message to the client - and lets the client display the ANSI content. - - More kludge lines displayed (with the 'K' key), and - all message header lines are now displayed (with the - 'H' key). This is a sysop feature. - - Color configuration options for the kludge/header line - labels (hdrLineLabelColor) and kludge/header line values - (hdrLineValueColor) - - Bug fix related to interpreting colors from other BBS - software (WWIV, PCBoard, Wildcat, Celerity, Renegade) -1.04 2015-10-10 - New feature: Users can now download attached files, - whether uploaded to their mailbox in Synchronet or - attached via internet email. - - New feature: Sysops can save a message to the BBS - machine (using the Ctrl-S key combo). - - User experience improvements: Added a pause after - saving a message so that the user can see Synchronet's - message save screen before going back to the reader or - message list. Also, in the message list, PageDown now - goes to the last message when on the last page, and - similarly, PageUp goes to the first message when on the - first page. - - Updated the DDReadPersonalMail.js loadable module - script to start reading personal email in lister mode - by default, which is more in line with what Synchronet - does by default. That will let the user select a message - to read first. -1.03 2015-07-12 Bug fix: In Linux, when replying to the last message in - a sub-board during a newscan or in read mode, it would - not immediately refresh the messagebase information, so - it would not see the new message posted. This has been - fixed by closing the messagebase while the user is - posting a reply message and re-opening the messagebase - when the user is done posting the reply. -1.02 2015-06-10 Bug fix in DDScanMsgs.js: Switched to bbs.scan_msgs() - instead of bbs.scan_subs() for all other scan modes - besides SCAN_READ. Updated the version number to - reflect that; no change to the actual reader. -1.01 2015-05-17 Bug fix: The enhanced reader header file is now correctly - displayed even if the lengths of its lines are - inconsistent. -1.00 2015-05-06 Initial release diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js index 88db114104a676c4393b60a6b65d4568b9d9508c..14f3f96e0e6182834deb6e81d89fad9e8bbab245 100644 --- a/xtrn/DDMsgReader/DDMsgReader.js +++ b/xtrn/DDMsgReader/DDMsgReader.js @@ -124,7 +124,10 @@ * Fix: For indexed read mode (not doing a newscan), when choosing a sub-board to * read, the correct (first unread) message is displayed. Also, the user's scan * pointer is also updated to the last_read pointer. - * 2024-01-06 Eric Oulashin Version 1.93a + * 2024-01-07 Eric Oulashin Version 1.93a + * New operator option for read mode: Add author email to email.can. + * New command-line option: -indexModeScope, which can specify the indexed + * reader scope (group/all) without prompting the user. * Releasing this version */ @@ -232,7 +235,7 @@ var hexdump = load('hexdump_lib.js'); // Reader version information var READER_VERSION = "1.93a"; -var READER_DATE = "2024-01-06"; +var READER_DATE = "2024-01-07"; // Keyboard key codes for displaying on the screen var UP_ARROW = ascii(24); @@ -574,14 +577,26 @@ if (gDoDDMR) { console.attributes = "N"; console.crlf(); - console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: "); - var scopeChar = console.getkeys("GA").toString(); + var scopeChar = ""; + if (typeof(gCmdLineArgVals.indexmodescope) === "string") + { + var argScopeLower = gCmdLineArgVals.indexmodescope.toLowerCase(); + if (argScopeLower == "group" || argScopeLower == "grp") + scopeChar = "G"; + else if (argScopeLower == "all") + scopeChar = "A"; + } + if (scopeChar == "") + { + console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: "); + scopeChar = console.getkeys("GA").toString(); + } if (typeof(scopeChar) === "string" && scopeChar != "") { var scanScope = SCAN_SCOPE_ALL; if (scopeChar == "G") scanScope = SCAN_SCOPE_GROUP; - else if (scopeChar == "G") + else if (scopeChar == "A") scanScope = SCAN_SCOPE_ALL; msgReader.DoIndexedMode(scanScope, false); } @@ -1082,10 +1097,10 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs) userSettings: CTRL_U, validateMsg: "A", // Only if the user is a sysop quickValUser: CTRL_Q, + threadView: "*", // TODO: Implement this operatorMenu: CTRL_O, showMsgHex: "X", - hexDump: CTRL_X, - threadView: "*" // TODO: Implement this + hexDump: CTRL_X }; //if (user.is_sysop) // this.enhReaderKeys.validateMsg = "A"; @@ -1576,7 +1591,8 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs) showMsgHex: 5, saveMsgHexToFile: 6, quickValUser: 7, - addAuthorToTwitList: 8 + addAuthorToTwitList: 8, + addAuthorEmailToEmailFilter: 9 }; // For indexed mode, whether to set the indexed mode menu item index to 1 more when showing @@ -6448,6 +6464,24 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); } break; + case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter: + var fromEmailAddr = ""; + if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0) + { + if (msgHeader.from_net_type == NET_INTERNET) + fromEmailAddr = msgHeader.from_net_addr; + else + fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr; + } + var promptTxt = format("Add %s to global email filter", fromEmailAddr); + if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true)) + { + var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!" + writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true); + console.attributes = "N"; + this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); + } + break; } } } @@ -7768,6 +7802,25 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg console.pause(); } break; + case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter: + var fromEmailAddr = ""; + if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0) + { + if (msgHeader.from_net_type == NET_INTERNET) + fromEmailAddr = msgHeader.from_net_addr; + else + fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr; + } + var promptTxt = format("Add %s to global email filter", fromEmailAddr); + if (!console.noyes(promptTxt)) + { + var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!" + console.print(statusMsg); + console.attributes = "N"; + console.crlf(); + console.pause(); + } + break; } } } @@ -7859,7 +7912,7 @@ function DigDistMsgReader_CreateReadModeOpMenu() var subBoardIsModerated = (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated); var opMenuWidth = 35; - var opMenuHeight = 10; + var opMenuHeight = 11; if (subBoardIsModerated) ++opMenuHeight; var opMenuX = Math.floor(console.screen_columns/2) - Math.floor(opMenuWidth/2); @@ -7882,6 +7935,7 @@ function DigDistMsgReader_CreateReadModeOpMenu() opMenu.Add("&E: Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile); opMenu.Add("&A: Quick validate the user", this.readerOpMenuOptValues.quickValUser); opMenu.Add("&I: Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList); + opMenu.Add("&M: Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter); // Use cyan for the item color, and cyan with blue background for selected item color opMenu.colors.itemColor = "\x01n\x01c"; opMenu.colors.selectedItemColor = "\x01n\x01c\x014"; @@ -7896,6 +7950,7 @@ function DigDistMsgReader_CreateReadModeOpMenu() opMenu.Add("Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile); opMenu.Add("Quick validate the user", this.readerOpMenuOptValues.quickValUser); opMenu.Add("Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList); + opMenu.Add("Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter); // Use green for the item color and high cyan for the item number color opMenu.colors.itemColor = "\x01n\x01g"; opMenu.colors.itemNumColor = "\x01n\x01c\x01h"; @@ -23999,6 +24054,69 @@ function entryExistsInTwitList(pStr) return entryExists; } +// Adds to the global email filter (email.can). +// +// Parameters: +// pEmailAddr: An email address +// +// Return value: Boolean - Whether or not this was successful +function addToGlobalEmailFilter(pEmailAddr) +{ + if (typeof(pEmailAddr) !== "string") + return false; + + var wasSuccessful = true; + if (!entryExistsInGlobalEmailFilter(pEmailAddr)) + { + wasSuccessful = false; + var filterFile = new File(system.text_dir + "email.can"); + //if (filterFile.open(filterFile.exists ? "r+" : "w+")) + if (filterFile.open("a")) + { + wasSuccessful = filterFile.writeln(pEmailAddr); + filterFile.close(); + } + } + return wasSuccessful; +} +// Returns whether an entry exists in the global email filter (email.can). +// +// Parameters: +// pEmailAddr: An entry to check in the twit list +// +// Return value: Boolean - Whether or not the given string exists in the twit list +function entryExistsInGlobalEmailFilter(pEmailAddr) +{ + if (typeof(pEmailAddr) !== "string") + return false; + + var entryExists = false; + var filterFile = new File(system.text_dir + "email.can"); + if (filterFile.open("r")) + { + while (!filterFile.eof && !entryExists) + { + //// Read the next line from the config file. + var fileLine = filterFile.readln(2048); + // fileLine should be a string, but I've seen some cases + // where for some reason it isn't. If it's not a string, + // then continue onto the next line. + 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; + + // See if this line matches the given string + entryExists = (pEmailAddr == skipsp(truncsp(fileLine))); + } + + filterFile.close(); + } + return entryExists; +} + /////////////////////////////////////////////////////////////////////////////////// // For debugging: Writes some text on the screen at a given location with a given pause. diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt index 27ff1d99ed56d11924ad61f6fecd01e5dc768ead..1be848e6750091e7ff63bd5239763c522e3e91fb 100644 --- a/xtrn/DDMsgReader/readme.txt +++ b/xtrn/DDMsgReader/readme.txt @@ -1,6 +1,6 @@ Digital Distortion Message Reader Version 1.93a - Release date: 2024-01-06 + Release date: 2024-01-07 by @@ -312,6 +312,8 @@ The following are the command-line parameters supported by DDMsgReader.js: user wants to list sub-boards in the current group, or all sub-boards. This is intended to work if it is the only command-line option. +-indexModeScope: Specifies the scope (set of sub-boards) for indexed reader mode + with -indexedMode. Valid values are "group" or "all". -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) @@ -452,6 +454,14 @@ common message operations. - Start in indexed reader mode: ?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode +- Start in indexed reader mode for the sub-boards in the current group (without +prompting): +?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=group + +- Start in indexed reader mode for all sub-boards(without prompting): +?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=all + + - Text (keyword) search in the current sub-board, and list the messages found: ?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -startMode=list @@ -1248,11 +1258,15 @@ Indexed reader mode may also be started with the -indexedMode command-line parameter. For example, if you are using a JavaScript command shell: bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode"); With the above command-line parameter, DDMsgReader will show all sub-boards the -user is allowed to read and which they have in their newscan configuration. -If the user has enabled indexed mode for newscans, then during a newscan, it -will show sub-boards based on the user's chosen option for current -sub-board/group/all. - +user is allowed to read. It will prompt the user to use sub-boards in the +current group or all sub-boards. + +To have it start in indexed reader for the current group without prompting: +bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=group"); + +To have it start in indexed reader for all sub-boards without prompting: +bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=all"); + This is an example of the sub-board menu that appears in indexed mode - And from here, the user can choose a sub-board to read: diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt index 0869282b9762d9dd68e46355e8e02a6247b8f124..ead58f5b52e909c978a40472eef78378dec377e2 100644 --- a/xtrn/DDMsgReader/revision_history.txt +++ b/xtrn/DDMsgReader/revision_history.txt @@ -5,10 +5,15 @@ Revision History (change log) ============================= Version Date Description ------- ---- ----------- -1.93a 2024-01-06 Fix: For indexed read mode (not doing a newscan), when +1.93a 2024-01-07 Fix: For indexed read mode (not doing a newscan), when choosing a sub-board to read, the correct (first unread) message is displayed. Also, the user's scan pointer is also updated to the last_read pointer. + New operator option for read mode: Add author email to + email.can + New command-line option: -indexModeScope, which can + specify the indexed reader scope (group/all) without + prompting the user. 1.93 2024-01-01 New user-toggleable behavior: Show indexed menu after reading all new messages Also, indexed reader mode (started with the -indexedMode