diff --git a/code/modules/NTNet/network.dm b/code/modules/NTNet/network.dm index ae8948dafba..e7d64f796ab 100644 --- a/code/modules/NTNet/network.dm +++ b/code/modules/NTNet/network.dm @@ -202,6 +202,11 @@ if(filename == P.filename) return P +/datum/ntnet/proc/get_chat_channel_by_id(id) + for(var/datum/ntnet_conversation/chan in chat_channels) + if(chan.id == id) + return chan + // Resets the IDS alarm /datum/ntnet/proc/resetIDS() intrusion_detection_alarm = FALSE diff --git a/code/modules/modular_computers/NTNet/NTNRC/conversation.dm b/code/modules/modular_computers/NTNet/NTNRC/conversation.dm index 20ad05b8e37..27eb3a6ae97 100644 --- a/code/modules/modular_computers/NTNet/NTNRC/conversation.dm +++ b/code/modules/modular_computers/NTNet/NTNRC/conversation.dm @@ -1,3 +1,5 @@ +#define MAX_CHANNELS 1000 + /datum/ntnet_conversation var/id = null var/title = "Untitled Conversation" @@ -8,7 +10,11 @@ var/static/ntnrc_uid = 0 /datum/ntnet_conversation/New() - id = ntnrc_uid++ + id = ntnrc_uid + 1 + if(id > MAX_CHANNELS) + qdel(src) + return + ntnrc_uid = id if(SSnetworks.station_network) SSnetworks.station_network.chat_channels.Add(src) ..() @@ -66,3 +72,5 @@ add_status_message("[client.username] has changed channel title from [title] to [newtitle]") title = newtitle + +#undef MAX_CHANNELS diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index 95c635b771f..8d2aabfba36 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -71,7 +71,7 @@ // Check if the user can run program. Only humans can operate computer. Automatically called in run_program() // User has to wear their ID for ID Scan to work. // Can also be called manually, with optional parameter being access_to_check to scan the user's ID -/datum/computer_file/program/proc/can_run(mob/user, loud = 0, access_to_check, transfer = 0) +/datum/computer_file/program/proc/can_run(mob/user, loud = FALSE, access_to_check, transfer = FALSE) // Defaults to required_access if(!access_to_check) if(transfer && transfer_access) @@ -79,16 +79,16 @@ else access_to_check = required_access if(!access_to_check) // No required_access, allow it. - return 1 + return TRUE if(!transfer && computer && (computer.obj_flags & EMAGGED)) //emags can bypass the execution locks but not the download ones. - return 1 + return TRUE if(IsAdminGhost(user)) - return 1 + return TRUE if(issilicon(user)) - return 1 + return TRUE if(ishuman(user)) var/obj/item/card/id/D @@ -102,14 +102,14 @@ if(!I && !D) if(loud) to_chat(user, "\The [computer] flashes an \"RFID Error - Unable to scan ID\" warning.") - return 0 + return FALSE if(I) if(access_to_check in I.GetAccess()) - return 1 + return TRUE else if(D) if(access_to_check in D.GetAccess()) - return 1 + return TRUE if(loud) to_chat(user, "\The [computer] flashes an \"Access Denied\" warning.") return 0 diff --git a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm index fc86892cfaa..d8b3f96f42e 100644 --- a/code/modules/modular_computers/file_system/programs/ntnrc_client.dm +++ b/code/modules/modular_computers/file_system/programs/ntnrc_client.dm @@ -10,116 +10,108 @@ ui_header = "ntnrc_idle.gif" available_on_ntnet = 1 tgui_id = "ntos_net_chat" + ui_x = 900 + ui_y = 675 - var/last_message = null // Used to generate the toolbar icon + var/last_message // Used to generate the toolbar icon var/username - var/datum/ntnet_conversation/channel = null - var/operator_mode = 0 // Channel operator mode - var/netadmin_mode = 0 // Administrator mode (invisible to other users + bypasses passwords) + var/active_channel + var/list/channel_history = list() + var/operator_mode = FALSE // Channel operator mode + var/netadmin_mode = FALSE // Administrator mode (invisible to other users + bypasses passwords) /datum/computer_file/program/chatclient/New() username = "DefaultUser[rand(100, 999)]" /datum/computer_file/program/chatclient/ui_act(action, params) if(..()) - return 1 + return + var/datum/ntnet_conversation/channel = SSnetworks.station_network.get_chat_channel_by_id(active_channel) + var/authed = FALSE + if(channel && ((channel.operator == src) || netadmin_mode)) + authed = TRUE switch(action) if("PRG_speak") - . = 1 - if(!channel) - return 1 - var/mob/living/user = usr - var/message = reject_bad_text(input(user, "Enter message or leave blank to cancel: ")) - if(!message || !channel) + if(!channel || isnull(active_channel)) return + var/message = reject_bad_text(params["message"]) + if(!message) + return + if(channel.password && !(src in channel.clients)) + if(channel.password == message) + channel.add_client(src) + return TRUE + channel.add_message(message, username) + var/mob/living/user = usr user.log_talk(message, LOG_CHAT, tag="as [username] to channel [channel.title]") - + return TRUE if("PRG_joinchannel") - . = 1 - var/datum/ntnet_conversation/C - for(var/datum/ntnet_conversation/chan in SSnetworks.station_network.chat_channels) - if(chan.id == text2num(params["id"])) - C = chan - break - - if(!C) - return 1 + var/new_target = text2num(params["id"]) + if(isnull(new_target) || new_target == active_channel) + return if(netadmin_mode) - channel = C // Bypasses normal leave/join and passwords. Technically makes the user invisible to others. - return 1 + active_channel = new_target // Bypasses normal leave/join and passwords. Technically makes the user invisible to others. + return TRUE - if(C.password) - var/mob/living/user = usr - var/password = reject_bad_text(input(user,"Access Denied. Enter password:")) - if(C && (password == C.password)) - C.add_client(src) - channel = C - return 1 - C.add_client(src) - channel = C + active_channel = new_target + channel = SSnetworks.station_network.get_chat_channel_by_id(new_target) + if(!(src in channel.clients) && !channel.password) + channel.add_client(src) + return TRUE if("PRG_leavechannel") - . = 1 if(channel) channel.remove_client(src) - channel = null + active_channel = null + return TRUE if("PRG_newchannel") - . = 1 - var/mob/living/user = usr - var/channel_title = reject_bad_text(input(user,"Enter channel name or leave blank to cancel:")) + var/channel_title = reject_bad_text(params["new_channel_name"]) if(!channel_title) return - var/datum/ntnet_conversation/C = new/datum/ntnet_conversation() + var/datum/ntnet_conversation/C = new /datum/ntnet_conversation() C.add_client(src) C.operator = src - channel = C C.title = channel_title + active_channel = C.id + return TRUE if("PRG_toggleadmin") - . = 1 if(netadmin_mode) - netadmin_mode = 0 + netadmin_mode = FALSE if(channel) channel.remove_client(src) // We shouldn't be in channel's user list, but just in case... - channel = null - return 1 + return TRUE var/mob/living/user = usr - if(can_run(usr, 1, ACCESS_NETWORK)) - if(channel) - var/response = alert(user, "Really engage admin-mode? You will be disconnected from your current channel!", "NTNRC Admin mode", "Yes", "No") - if(response == "Yes") - if(channel) - channel.remove_client(src) - channel = null - else - return - netadmin_mode = 1 + if(can_run(user, TRUE, ACCESS_NETWORK)) + for(var/C in SSnetworks.station_network.chat_channels) + var/datum/ntnet_conversation/chan = C + chan.remove_client(src) + netadmin_mode = TRUE + return TRUE if("PRG_changename") - . = 1 - var/mob/living/user = usr - var/newname = sanitize(input(user,"Enter new nickname or leave blank to cancel:")) + var/newname = sanitize(params["new_name"]) if(!newname) - return 1 - if(channel) - channel.add_status_message("[username] is now known as [newname].") + return + for(var/C in SSnetworks.station_network.chat_channels) + var/datum/ntnet_conversation/chan = C + if(src in chan.clients) + chan.add_status_message("[username] is now known as [newname].") username = newname - + return TRUE if("PRG_savelog") - . = 1 if(!channel) return - var/mob/living/user = usr - var/logname = stripped_input(user,"Enter desired logfile name (.log) or leave blank to cancel:") - if(!logname || !channel) - return 1 - var/datum/computer_file/data/logfile = new/datum/computer_file/data/logfile() + var/logname = stripped_input(params["log_name"]) + if(!logname) + return + var/datum/computer_file/data/logfile = new /datum/computer_file/data/logfile() // Now we will generate HTML-compliant file that can actually be viewed/printed. logfile.filename = logname logfile.stored_data = "\[b\]Logfile dump from NTNRC channel [channel.title]\[/b\]\[BR\]" for(var/logstring in channel.messages) - logfile.stored_data += "[logstring]\[BR\]" - logfile.stored_data += "\[b\]Logfile dump completed.\[/b\]" + logfile.stored_data = "[logfile.stored_data][logstring]\[BR\]" + logfile.stored_data = "[logfile.stored_data]\[b\]Logfile dump completed.\[/b\]" logfile.calculate_size() var/obj/item/computer_hardware/hard_drive/hard_drive = computer.all_components[MC_HDD] if(!computer || !hard_drive || !hard_drive.store_file(logfile)) @@ -130,94 +122,113 @@ computer.visible_message("\The [computer] shows an \"I/O Error - Hard drive connection error\" warning.") else // In 99.9% cases this will mean our HDD is full computer.visible_message("\The [computer] shows an \"I/O Error - Hard drive may be full. Please free some space and try again. Required space: [logfile.size]GQ\" warning.") + return TRUE if("PRG_renamechannel") - . = 1 - if(!operator_mode || !channel) - return 1 - var/mob/living/user = usr - var/newname = reject_bad_text(input(user, "Enter new channel name or leave blank to cancel:")) + if(!authed) + return + var/newname = reject_bad_text(params["new_name"]) if(!newname || !channel) return channel.add_status_message("Channel renamed from [channel.title] to [newname] by operator.") channel.title = newname + return TRUE if("PRG_deletechannel") - . = 1 - if(channel && ((channel.operator == src) || netadmin_mode)) + if(authed) qdel(channel) - channel = null + active_channel = null + return TRUE if("PRG_setpassword") - . = 1 - if(!channel || ((channel.operator != src) && !netadmin_mode)) - return 1 + if(!authed) + return - var/mob/living/user = usr - var/newpassword = sanitize(input(user, "Enter new password for this channel. Leave blank to cancel, enter 'nopassword' to remove password completely:")) - if(!channel || !newpassword || ((channel.operator != src) && !netadmin_mode)) - return 1 + var/new_password = sanitize(params["new_password"]) + if(!authed) + return - if(newpassword == "nopassword") - channel.password = "" - else - channel.password = newpassword + channel.password = new_password + return TRUE /datum/computer_file/program/chatclient/process_tick() - ..() + . = ..() + var/datum/ntnet_conversation/channel = SSnetworks.station_network.get_chat_channel_by_id(active_channel) if(program_state != PROGRAM_STATE_KILLED) ui_header = "ntnrc_idle.gif" if(channel) // Remember the last message. If there is no message in the channel remember null. - last_message = channel.messages.len ? channel.messages[channel.messages.len - 1] : null + last_message = length(channel.messages) ? channel.messages[length(channel.messages)] : null else last_message = null - return 1 - if(channel && channel.messages && channel.messages.len) - ui_header = last_message == channel.messages[channel.messages.len - 1] ? "ntnrc_idle.gif" : "ntnrc_new.gif" + return TRUE + if(channel?.messages?.len) + ui_header = last_message == channel.messages[length(channel.messages)] ? "ntnrc_idle.gif" : "ntnrc_new.gif" else ui_header = "ntnrc_idle.gif" /datum/computer_file/program/chatclient/kill_program(forced = FALSE) - if(channel) + for(var/C in SSnetworks.station_network.chat_channels) + var/datum/ntnet_conversation/channel = C channel.remove_client(src) - channel = null ..() +/datum/computer_file/program/chatclient/ui_static_data(mob/user) + var/list/data = list() + data["can_admin"] = can_run(user, FALSE, ACCESS_NETWORK) + return data + /datum/computer_file/program/chatclient/ui_data(mob/user) if(!SSnetworks.station_network || !SSnetworks.station_network.chat_channels) - return + return list() var/list/data = list() data = get_header_data() + var/list/all_channels = list() + for(var/C in SSnetworks.station_network.chat_channels) + var/datum/ntnet_conversation/conv = C + if(conv && conv.title) + all_channels.Add(list(list( + "chan" = conv.title, + "id" = conv.id + ))) + data["all_channels"] = all_channels + data["active_channel"] = active_channel + data["username"] = username data["adminmode"] = netadmin_mode + var/datum/ntnet_conversation/channel = SSnetworks.station_network.get_chat_channel_by_id(active_channel) if(channel) data["title"] = channel.title - var/list/messages[0] - for(var/M in channel.messages) - messages.Add(list(list( - "msg" = M - ))) - data["messages"] = messages - var/list/clients[0] + var/authed = FALSE + if(!channel.password) + authed = TRUE + if(netadmin_mode) + authed = TRUE + var/list/clients = list() for(var/C in channel.clients) + if(C == src) + authed = TRUE var/datum/computer_file/program/chatclient/cl = C clients.Add(list(list( "name" = cl.username ))) - data["clients"] = clients - operator_mode = (channel.operator == src) ? 1 : 0 - data["is_operator"] = operator_mode || netadmin_mode - - else // Channel selection screen - var/list/all_channels[0] - for(var/C in SSnetworks.station_network.chat_channels) - var/datum/ntnet_conversation/conv = C - if(conv && conv.title) - all_channels.Add(list(list( - "chan" = conv.title, - "id" = conv.id + data["authed"] = authed + //no fishing for ui data allowed + if(authed) + data["clients"] = clients + var/list/messages = list() + for(var/M in channel.messages) + messages.Add(list(list( + "msg" = M ))) - data["all_channels"] = all_channels + data["messages"] = messages + data["is_operator"] = (channel.operator == src) || netadmin_mode + else + data["clients"] = list() + data["messages"] = list() + else + data["clients"] = list() + data["authed"] = FALSE + data["messages"] = list() return data diff --git a/tgui-next/README.md b/tgui-next/README.md index a8435efc30a..23426fc6e55 100644 --- a/tgui-next/README.md +++ b/tgui-next/README.md @@ -315,6 +315,27 @@ Props: - See inherited props: [Button](#button) - `checked: boolean` - Boolean value, which marks the checkbox as checked. +### `Button.Confirm` + +A button with a an extra confirmation step, using native button component. + +Props: + +- See inherited props: [Button](#button) +- `confirmMessage: string` - Text to display after first click; defaults to "Confirm?" +- `confirmColor: string` - Color to display after first click; default to "bad" + +### `Button.Input` + +A button that turns into an input box after the first click. Turns back into a button after the user hits enter, defocuses, or hits escape. Enter and defocus commit, while escape cancels. + +Props: + - See inherited props: [Box](#box) + - `fluid`: fill availible horizontal space + - `onCommit: (e, value) => void`: function that is called after the user defocuses the input or presses enter + - `currentValue: string`: default string to display when the input is shown + - `defaultValue: string`: default value emitted if the user leaves the box blank when hitting enter or defocusing. If left undefined, will cancel the change on a blank defocus/enter + ### `Collapsible` Displays contents when open, acts as a fluid button when closed. Click to toggle, closed by default. @@ -519,6 +540,7 @@ Props: - `value: string` - Value of an input. - `placeholder: string` - Text placed into Input box when value is otherwise nothing. Clears automatically when focused. - `fluid: boolean` - Fill all available horizontal space. +- `selfClear: boolean` - Clear after hitting enter, as well as remain focused when this happens. Useful for things like chat inputs - `onChange: (e, value) => void` - An event, which fires when you commit the text by either unfocusing the input box, or by pressing the Enter key. - `onInput: (e, value) => void` - An event, which fires on every keypress. diff --git a/tgui-next/packages/tgui/components/Box.js b/tgui-next/packages/tgui/components/Box.js index fdeb2d910f3..5fa678681ce 100644 --- a/tgui-next/packages/tgui/components/Box.js +++ b/tgui-next/packages/tgui/components/Box.js @@ -57,6 +57,13 @@ const mapColorPropTo = attrName => (style, value) => { const styleMapperByPropName = { // Direct mapping position: mapRawPropTo('position'), + overflow: mapRawPropTo('overflow'), + overflowX: mapRawPropTo('overflow-x'), + overflowY: mapRawPropTo('overflow-y'), + top: mapUnitPropTo('top'), + bottom: mapUnitPropTo('bottom'), + left: mapUnitPropTo('left'), + right: mapUnitPropTo('right'), width: mapUnitPropTo('width'), minWidth: mapUnitPropTo('min-width'), maxWidth: mapUnitPropTo('max-width'), diff --git a/tgui-next/packages/tgui/components/Button.js b/tgui-next/packages/tgui/components/Button.js index 7fc52ac6f2c..5ed0c8648bc 100644 --- a/tgui-next/packages/tgui/components/Button.js +++ b/tgui-next/packages/tgui/components/Button.js @@ -1,11 +1,14 @@ import { classes, pureComponentHooks } from 'common/react'; -import { tridentVersion } from '../byond'; +import { tridentVersion, act } from '../byond'; import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from '../hotkeys'; import { createLogger } from '../logging'; import { refocusLayout } from '../refocus'; import { Box } from './Box'; import { Icon } from './Icon'; import { Tooltip } from './Tooltip'; +import { Input } from './Input'; +import { Component, createRef } from 'inferno'; +import { Grid } from './Grid'; const logger = createLogger('Button'); @@ -107,3 +110,148 @@ export const ButtonCheckbox = props => { }; Button.Checkbox = ButtonCheckbox; + +export class ButtonConfirm extends Component { + constructor() { + super(); + this.state = { + clickedOnce: false, + }; + this.handleClick = () => { + if (this.state.clickedOnce) { + this.setClickedOnce(false); + } + }; + } + + setClickedOnce(clickedOnce) { + this.setState({ + clickedOnce, + }); + if (clickedOnce) { + setTimeout(() => window.addEventListener('click', this.handleClick)); + } + else { + window.removeEventListener('click', this.handleClick); + } + } + + render() { + const { + confirmMessage = "Confirm?", + confirmColor = "bad", + color, + content, + onClick, + ...rest + } = this.props; + return ( +