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 (
+