diff --git a/code/__DEFINES/keybindings_defines.dm b/code/__DEFINES/keybindings_defines.dm index cc1c80d83c5..04debd1a244 100644 --- a/code/__DEFINES/keybindings_defines.dm +++ b/code/__DEFINES/keybindings_defines.dm @@ -14,6 +14,7 @@ #define KB_CATEGORY_EMOTE_SILICON 14 #define KB_CATEGORY_EMOTE_ANIMAL 15 #define KB_CATEGORY_EMOTE_CUSTOM 16 +#define KB_CATEGORY_COMMUNICATION 17 #define KB_CATEGORY_UNSORTED 1000 ///Max length of a keypress command before it's considered to be a forged packet/bogus command diff --git a/code/__DEFINES/preferences_defines.dm b/code/__DEFINES/preferences_defines.dm index d31152b3c7c..da31608086d 100644 --- a/code/__DEFINES/preferences_defines.dm +++ b/code/__DEFINES/preferences_defines.dm @@ -1,73 +1,74 @@ //Preference toggles -#define SOUND_ADMINHELP 1 -#define SOUND_MIDI 2 -#define SOUND_AMBIENCE 4 -#define SOUND_LOBBY 8 -#define SOUND_HEARTBEAT 32 -#define SOUND_BUZZ 64 -#define SOUND_INSTRUMENTS 128 -#define SOUND_MENTORHELP 256 -#define SOUND_DISCO 512 -#define SOUND_AI_VOICE 1024 +#define SOUND_ADMINHELP 1 +#define SOUND_MIDI 2 +#define SOUND_AMBIENCE 4 +#define SOUND_LOBBY 8 +#define SOUND_HEARTBEAT 32 +#define SOUND_BUZZ 64 +#define SOUND_INSTRUMENTS 128 +#define SOUND_MENTORHELP 256 +#define SOUND_DISCO 512 +#define SOUND_AI_VOICE 1024 #define SOUND_PRAYERNOTIFY 2048 #define SOUND_DEFAULT (SOUND_ADMINHELP|SOUND_MIDI|SOUND_AMBIENCE|SOUND_LOBBY|SOUND_HEARTBEAT|SOUND_BUZZ|SOUND_INSTRUMENTS|SOUND_MENTORHELP|SOUND_DISCO|SOUND_AI_VOICE|SOUND_PRAYERNOTIFY) -#define PREFTOGGLE_CHAT_OOC 1 -#define PREFTOGGLE_CHAT_DEAD 2 -#define PREFTOGGLE_CHAT_GHOSTEARS 4 -#define PREFTOGGLE_CHAT_GHOSTSIGHT 8 -#define PREFTOGGLE_CHAT_PRAYER 16 -#define PREFTOGGLE_CHAT_RADIO 32 -#define PREFTOGGLE_AZERTY 64 // obsolete -#define PREFTOGGLE_CHAT_DEBUGLOGS 128 -#define PREFTOGGLE_CHAT_LOOC 256 -#define PREFTOGGLE_CHAT_GHOSTRADIO 512 -#define PREFTOGGLE_SHOW_TYPING 1024 -#define PREFTOGGLE_DISABLE_SCOREBOARD 2048 +#define PREFTOGGLE_CHAT_OOC 1 +#define PREFTOGGLE_CHAT_DEAD 2 +#define PREFTOGGLE_CHAT_GHOSTEARS 4 +#define PREFTOGGLE_CHAT_GHOSTSIGHT 8 +#define PREFTOGGLE_CHAT_PRAYER 16 +#define PREFTOGGLE_CHAT_RADIO 32 +// #define PREFTOGGLE_AZERTY 64 // obsolete +#define PREFTOGGLE_CHAT_DEBUGLOGS 128 +#define PREFTOGGLE_CHAT_LOOC 256 +#define PREFTOGGLE_CHAT_GHOSTRADIO 512 +#define PREFTOGGLE_SHOW_TYPING 1024 +#define PREFTOGGLE_DISABLE_SCOREBOARD 2048 // #define PREFTOGGLE_DISABLE_KARMA_REMINDER 4096 // Defunct as of 2023-03-12 -#define PREFTOGGLE_MEMBER_PUBLIC 8192 -#define PREFTOGGLE_CHAT_NO_ADMINLOGS 16384 -#define PREFTOGGLE_DONATOR_PUBLIC 32768 -#define PREFTOGGLE_CHAT_NO_TICKETLOGS 65536 -#define PREFTOGGLE_UI_DARKMODE 131072 +#define PREFTOGGLE_MEMBER_PUBLIC 8192 +#define PREFTOGGLE_CHAT_NO_ADMINLOGS 16384 +#define PREFTOGGLE_DONATOR_PUBLIC 32768 +#define PREFTOGGLE_CHAT_NO_TICKETLOGS 65536 +// #define PREFTOGGLE_UI_DARKMODE 131072 // Defunct as of 2024-01-29 // #define PREFTOGGLE_DISABLE_KARMA 262144 // Defunct as of 2023-03-12 -#define PREFTOGGLE_CHAT_NO_MENTORTICKETLOGS 524288 -#define PREFTOGGLE_TYPING_ONCE 1048576 -#define PREFTOGGLE_AMBIENT_OCCLUSION 2097152 -#define PREFTOGGLE_CHAT_GHOSTPDA 4194304 -#define PREFTOGGLE_NUMPAD_TARGET 8388608 // Made obsolete with key bindings +#define PREFTOGGLE_CHAT_NO_MENTORTICKETLOGS 524288 +// #define PREFTOGGLE_TYPING_ONCE 1048576 // Defunct as of 2024-01-29 +#define PREFTOGGLE_AMBIENT_OCCLUSION 2097152 +#define PREFTOGGLE_CHAT_GHOSTPDA 4194304 +// #define PREFTOGGLE_NUMPAD_TARGET 8388608 // Made obsolete with key bindings -#define TOGGLES_TOTAL 16777215 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. +#define TOGGLES_TOTAL 16777215 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. -#define TOGGLES_DEFAULT (PREFTOGGLE_CHAT_OOC|PREFTOGGLE_CHAT_DEAD|PREFTOGGLE_CHAT_GHOSTEARS|PREFTOGGLE_CHAT_GHOSTSIGHT|PREFTOGGLE_CHAT_PRAYER|PREFTOGGLE_CHAT_RADIO|PREFTOGGLE_CHAT_DEBUGLOGS|PREFTOGGLE_CHAT_LOOC|PREFTOGGLE_MEMBER_PUBLIC|PREFTOGGLE_DONATOR_PUBLIC|PREFTOGGLE_AMBIENT_OCCLUSION|PREFTOGGLE_CHAT_GHOSTPDA|PREFTOGGLE_NUMPAD_TARGET) +#define TOGGLES_DEFAULT (PREFTOGGLE_CHAT_OOC|PREFTOGGLE_CHAT_DEAD|PREFTOGGLE_CHAT_GHOSTEARS|PREFTOGGLE_CHAT_GHOSTSIGHT|PREFTOGGLE_CHAT_PRAYER|PREFTOGGLE_CHAT_RADIO|PREFTOGGLE_CHAT_DEBUGLOGS|PREFTOGGLE_CHAT_LOOC|PREFTOGGLE_MEMBER_PUBLIC|PREFTOGGLE_DONATOR_PUBLIC|PREFTOGGLE_AMBIENT_OCCLUSION|PREFTOGGLE_CHAT_GHOSTPDA) // toggles_2 variables. These MUST be prefixed with PREFTOGGLE_2 -#define PREFTOGGLE_2_RANDOMSLOT (1<<0) // 1 -#define PREFTOGGLE_2_FANCYUI (1<<1) // 2 -#define PREFTOGGLE_2_ITEMATTACK (1<<2) // 4 -#define PREFTOGGLE_2_WINDOWFLASHING (1<<3) // 8 -#define PREFTOGGLE_2_ANON (1<<4) // 16 -#define PREFTOGGLE_2_AFKWATCH (1<<5) // 32 -#define PREFTOGGLE_2_RUNECHAT (1<<6) // 64 -#define PREFTOGGLE_2_DEATHMESSAGE (1<<7) // 128 -#define PREFTOGGLE_2_EMOTE_BUBBLE (1<<8) // 256 +#define PREFTOGGLE_2_RANDOMSLOT (1<<0) // 1 +#define PREFTOGGLE_2_FANCYUI (1<<1) // 2 +#define PREFTOGGLE_2_ITEMATTACK (1<<2) // 4 +#define PREFTOGGLE_2_WINDOWFLASHING (1<<3) // 8 +#define PREFTOGGLE_2_ANON (1<<4) // 16 +#define PREFTOGGLE_2_AFKWATCH (1<<5) // 32 +#define PREFTOGGLE_2_RUNECHAT (1<<6) // 64 +#define PREFTOGGLE_2_DEATHMESSAGE (1<<7) // 128 +// #define PREFTOGGLE_2_EMOTE_BUBBLE (1<<8) // 256 - Defunct as of 2024-01-29 // Yes I know this being an "enable to disable" is misleading, but it avoids having to tweak all existing pref entries -#define PREFTOGGLE_2_REVERB_DISABLE (1<<9) // 512 -#define PREFTOGGLE_2_FORCE_WHITE_RUNECHAT (1<<10) // 1024 -#define PREFTOGGLE_2_SIMPLE_STAT_PANEL (1<<11) // 2048 -#define PREFTOGGLE_2_SEE_ITEM_OUTLINES (1<<12) // 4096 -#define PREFTOGGLE_2_HIDE_ITEM_TOOLTIPS (1<<13) // 8192 -#define PREFTOGGLE_2_THOUGHT_BUBBLE (1<<14) // 16384 -#define PREFTOGGLE_2_MC_TABS (1<<15) // 32768 -#define PREFTOGGLE_2_DANCE_DISCO (1<<16) // 65536 -#define PREFTOGGLE_2_MOD_ACTIVATION_METHOD (1<<17) // 131072 -#define PREFTOGGLE_2_PARALLAX_IN_DARKNESS (1<<18) // 262144 -#define PREFTOGGLE_2_DISABLE_TGUI_LISTS (1<<19) // 524288 +#define PREFTOGGLE_2_REVERB_DISABLE (1<<9) // 512 +#define PREFTOGGLE_2_FORCE_WHITE_RUNECHAT (1<<10) // 1024 +#define PREFTOGGLE_2_SIMPLE_STAT_PANEL (1<<11) // 2048 +#define PREFTOGGLE_2_SEE_ITEM_OUTLINES (1<<12) // 4096 +#define PREFTOGGLE_2_HIDE_ITEM_TOOLTIPS (1<<13) // 8192 +#define PREFTOGGLE_2_THOUGHT_BUBBLE (1<<14) // 16384 +#define PREFTOGGLE_2_MC_TABS (1<<15) // 32768 +#define PREFTOGGLE_2_DANCE_DISCO (1<<16) // 65536 +#define PREFTOGGLE_2_MOD_ACTIVATION_METHOD (1<<17) // 131072 +#define PREFTOGGLE_2_PARALLAX_IN_DARKNESS (1<<18) // 262144 +#define PREFTOGGLE_2_DISABLE_TGUI_LISTS (1<<19) // 524288 +#define PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE (1<<20) // 1048576 -#define TOGGLES_2_TOTAL 1048575 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. +#define TOGGLES_2_TOTAL 2097151 // If you add or remove a preference toggle above, make sure you update this define with the total value of the toggles combined. -#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING|PREFTOGGLE_2_RUNECHAT|PREFTOGGLE_2_DEATHMESSAGE|PREFTOGGLE_2_EMOTE_BUBBLE|PREFTOGGLE_2_SEE_ITEM_OUTLINES|PREFTOGGLE_2_THOUGHT_BUBBLE|PREFTOGGLE_2_DANCE_DISCO|PREFTOGGLE_2_MOD_ACTIVATION_METHOD) +#define TOGGLES_2_DEFAULT (PREFTOGGLE_2_FANCYUI|PREFTOGGLE_2_ITEMATTACK|PREFTOGGLE_2_WINDOWFLASHING|PREFTOGGLE_2_RUNECHAT|PREFTOGGLE_2_DEATHMESSAGE|PREFTOGGLE_2_SEE_ITEM_OUTLINES|PREFTOGGLE_2_THOUGHT_BUBBLE|PREFTOGGLE_2_DANCE_DISCO|PREFTOGGLE_2_MOD_ACTIVATION_METHOD) // Sanity checks #if TOGGLES_TOTAL > 16777215 diff --git a/code/__DEFINES/speech_channels.dm b/code/__DEFINES/speech_channels.dm new file mode 100644 index 00000000000..68067d21aec --- /dev/null +++ b/code/__DEFINES/speech_channels.dm @@ -0,0 +1,11 @@ +// Used to direct channels to speak into. +#define SAY_CHANNEL "Say" +#define RADIO_CHANNEL "Radio" +#define WHISPER_CHANNEL "Whisper" +#define ME_CHANNEL "Me" +#define OOC_CHANNEL "OOC" +#define LOOC_CHANNEL "LOOC" +#define MENTOR_CHANNEL "Mentor" +#define ADMIN_CHANNEL "Admin" +#define DSAY_CHANNEL "Dsay" + diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index dbe9fa59587..d58196cb480 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -89,8 +89,6 @@ // Uses client.typing to check if the popup should appear or not /proc/typing_input(mob/user, message = "", title = "", default = "") - if(user.client.checkTyping()) // Prevent double windows - return null var/client/C = user.client // Save it in a var in case the client disconnects from the mob C.typing = TRUE var/msg = input(user, message, title, html_decode(default)) as text|null @@ -272,6 +270,10 @@ /proc/trim(text) return trim_left(trim_right(text)) +/// Returns a string that does not exceed max_length characters in size +/proc/trim_length(text, max_length) + return copytext_char(text, 1, max_length) + //Returns a string with the first element of the string capitalized. /proc/capitalize(t as text) return uppertext(copytext(t, 1, 2)) + copytext(t, 2) diff --git a/code/_globalvars/lists/keybindings_lists.dm b/code/_globalvars/lists/keybindings_lists.dm index 350c5c3ec75..836c1a0cbb5 100644 --- a/code/_globalvars/lists/keybindings_lists.dm +++ b/code/_globalvars/lists/keybindings_lists.dm @@ -2,6 +2,7 @@ GLOBAL_LIST_EMPTY(keybindings) GLOBAL_LIST_INIT(keybindings_groups, list( "Movement" = KB_CATEGORY_MOVEMENT, + "Communication" = KB_CATEGORY_COMMUNICATION, "Living" = KB_CATEGORY_LIVING, "General" = KB_CATEGORY_MOB, "General Emote" = KB_CATEGORY_EMOTE_GENERIC, diff --git a/code/datums/keybindings/admin_keybinds.dm b/code/datums/keybindings/admin_keybinds.dm index 19cb750a2d7..f2f9675f2aa 100644 --- a/code/datums/keybindings/admin_keybinds.dm +++ b/code/datums/keybindings/admin_keybinds.dm @@ -20,22 +20,6 @@ return SSdebugview.start_processing(C) -/datum/keybinding/admin/msay - name = "Msay" - keys = list("F4") - -/datum/keybinding/admin/msay/down(client/C) - . = ..() - C.get_mentor_say() - -/datum/keybinding/admin/asay - name = "Asay" - keys = list("F5") - -/datum/keybinding/admin/asay/down(client/C) - . = ..() - C.get_admin_say() - /datum/keybinding/admin/aghost name = "Aghost" keys = list("F6") @@ -68,11 +52,3 @@ /datum/keybinding/admin/invisimin/down(client/C) . = ..() C.invisimin() - -/datum/keybinding/admin/dsay - name = "Dsay" - keys = list("F10") - -/datum/keybinding/admin/dsay/down(client/C) - . = ..() - C.get_dead_say() diff --git a/code/datums/keybindings/client.dm b/code/datums/keybindings/client.dm index b9f6cc79a62..23a89404931 100644 --- a/code/datums/keybindings/client.dm +++ b/code/datums/keybindings/client.dm @@ -9,38 +9,6 @@ . = ..() C.adminhelp() -/datum/keybinding/client/ooc - name = "OOC" - keys = list("O") - -/datum/keybinding/client/ooc/down(client/C) - . = ..() - C.ooc() - -/datum/keybinding/client/looc - name = "Local OOC" - keys = list("L") - -/datum/keybinding/client/looc/down(client/C) - . = ..() - C.looc() - -/datum/keybinding/client/say - name = "Say" - keys = list("T") - -/datum/keybinding/client/say/down(client/C) - . = ..() - C.mob.say_wrapper() - -/datum/keybinding/client/me - name = "Me" - keys = list("M") - -/datum/keybinding/client/me/down(client/C) - . = ..() - C.mob.me_wrapper() - /datum/keybinding/client/toggle_min_hud name = "Toggle Minimal HUD" keys = list("F12") diff --git a/code/datums/keybindings/communication_keybinds.dm b/code/datums/keybindings/communication_keybinds.dm new file mode 100644 index 00000000000..b3ddd289fda --- /dev/null +++ b/code/datums/keybindings/communication_keybinds.dm @@ -0,0 +1,74 @@ +/datum/keybinding/client/communication + category = KB_CATEGORY_COMMUNICATION + /// Used to store special rights if required by a keybind, such as R_ADMIN + var/required_rights + /// Used to map muted categories to channels + var/mute_category = MUTE_OOC + +/datum/keybinding/client/communication/down(client/C) + . = ..() + if(required_rights && !check_rights(required_rights, FALSE, C.mob)) + return + + if(mute_category && check_mute(C.ckey, mute_category)) + to_chat(C, "You cannot use [name] (muted).", MESSAGE_TYPE_WARNING) + return + + winset(C, null, "command=[C.tgui_say_create_open_command(name)]") + +/datum/keybinding/client/communication/ooc + name = OOC_CHANNEL + keys = list("O") + +/datum/keybinding/client/communication/ooc/down(client/C) + if(check_rights(R_ADMIN, FALSE, C.mob)) // You may pass + return ..() + + if(!GLOB.ooc_enabled) + to_chat(C, "OOC is globally muted.", MESSAGE_TYPE_WARNING) + return + + if(!GLOB.dooc_enabled && C.mob.stat == DEAD) + to_chat(C, "OOC for dead mobs has been turned off.", MESSAGE_TYPE_WARNING) + return + + return ..() + +/datum/keybinding/client/communication/looc + name = LOOC_CHANNEL + keys = list("L") + +/datum/keybinding/client/communication/say + name = SAY_CHANNEL + keys = list("T") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/me + name = ME_CHANNEL + keys = list("M") + mute_category = MUTE_EMOTE + +/datum/keybinding/client/communication/whisper + name = WHISPER_CHANNEL + keys = list("U") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/radio + name = RADIO_CHANNEL + keys = list("Y") + mute_category = MUTE_IC + +/datum/keybinding/client/communication/msay + name = MENTOR_CHANNEL + keys = list("F4") + required_rights = R_MENTOR | R_ADMIN + +/datum/keybinding/client/communication/asay + name = ADMIN_CHANNEL + keys = list("F5") + required_rights = R_ADMIN + +/datum/keybinding/client/communication/dsay + name = DSAY_CHANNEL + keys = list("F10") + required_rights = R_ADMIN diff --git a/code/datums/keybindings/living_keybinds.dm b/code/datums/keybindings/living_keybinds.dm index 4dfb11f1073..47f59550ba4 100644 --- a/code/datums/keybindings/living_keybinds.dm +++ b/code/datums/keybindings/living_keybinds.dm @@ -21,17 +21,3 @@ . = ..() var/mob/living/M = C.mob M.resist() - -/datum/keybinding/living/whisper - name = "Whisper" - keys = list("U") - -/datum/keybinding/living/whisper/down(client/C) - var/mob/M = C.mob - M.set_typing_indicator(TRUE) - M.hud_typing = 1 - var/message = typing_input(M, "", "Whisper (text)") - M.hud_typing = 0 - M.set_typing_indicator(FALSE) - if(message) - M.whisper(message) diff --git a/code/game/verbs/ooc.dm b/code/game/verbs/ooc.dm index bd187cc3f6f..234fa06112a 100644 --- a/code/game/verbs/ooc.dm +++ b/code/game/verbs/ooc.dm @@ -6,10 +6,6 @@ GLOBAL_VAR_INIT(mentor_ooc_colour, "#00B0EB") GLOBAL_VAR_INIT(moderator_ooc_colour, "#184880") GLOBAL_VAR_INIT(admin_ooc_colour, "#b82e00") -//Checks if the client already has a text input open -/client/proc/checkTyping() - return (prefs.toggles & PREFTOGGLE_TYPING_ONCE && typing) - /client/verb/ooc(msg = "" as text) set name = "OOC" set category = "OOC" diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index caa9ed53890..73bf7ebdbb4 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -660,6 +660,7 @@ GLOBAL_LIST_INIT(view_runtimes_verbs, list( GLOB.de_mentors += ckey deadmin() verbs += /client/proc/readmin + update_active_keybindings() to_chat(src, "You are now a normal player.") SSblackbox.record_feedback("tally", "admin_verb", 1, "De-admin") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -749,6 +750,7 @@ GLOBAL_LIST_INIT(view_runtimes_verbs, list( var/client/C = GLOB.directory[ckey] D.associate(C) + update_active_keybindings() message_admins("[key_name_admin(usr)] re-adminned themselves.") log_admin("[key_name(usr)] re-adminned themselves.") GLOB.de_admins -= ckey diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index f5acca8a6b5..c647cfef350 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -124,12 +124,15 @@ /// The client's movement keybindings to directions, which work regardless of modifiers. var/list/movement_kb_dirs = list() - ///A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining + /// A lazy list of atoms we've examined in the last RECENT_EXAMINE_MAX_WINDOW (default 2) seconds, so that we will call [/atom/proc/examine_more] instead of [/atom/proc/examine] on them when examining var/list/recent_examines /// When to next alert admins that mouse macro use was attempted var/next_mouse_macro_warning + /// Assigned say modal of the client + var/datum/tgui_say/tgui_say + /client/vv_edit_var(var_name, var_value) switch(var_name) // I know we will never be in a world where admins are editing client vars to let people bypass TOS diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index a2323c88748..8cc125f02dc 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -254,7 +254,10 @@ /////////// /client/New(TopicData) var/tdata = TopicData //save this for later use - tgui_panel = new(src) + + tgui_panel = new(src, "browseroutput") + tgui_say = new(src, "tgui_say") + TopicData = null //Prevent calls to client.Topic from connect if(connection != "seeker") //Invalid connection type. @@ -392,6 +395,9 @@ // Initialize tgui panel tgui_panel.initialize() + // Initialize tgui say + tgui_say.initialize() + check_ip_intel() send_resources() SSchangelog.UpdatePlayerChangelogButton(src) @@ -955,31 +961,6 @@ #undef SSD_WARNING_TIMER - -// -/client/verb/resend_ui_resources() - set name = "Reload UI Resources" - set desc = "Reload your UI assets if they are not working" - set category = "Special Verbs" - - if(last_ui_resource_send > world.time) - to_chat(usr, "You requested your UI resource files too quickly. Please try again in [(last_ui_resource_send - world.time)/10] seconds.") - return - - var/choice = alert(usr, "This will reload your TGUI resources. If you have any open UIs, they will be closed. Are you sure?", "Resource Reloading", "Yes", "No") - if(choice == "Yes") - // 600 deciseconds = 1 minute - last_ui_resource_send = world.time + 60 SECONDS - - // Close their open UIs - SStgui.close_user_uis(usr) - - // Clear the user's cache so they get resent. - // This is not fully clearing their BYOND cache, just their assets sent from the server this round - sent_assets = list() - to_chat(usr, "UI resource files resent successfully. If you are still having issues, please try manually clearing your BYOND cache. This can be achieved by opening your BYOND launcher, pressing the cog in the top right, selecting preferences, going to the Games tab, and pressing 'Clear Cache'.") - - /** * Retrieves the BYOND accounts data from the BYOND servers * diff --git a/code/modules/client/preference/link_processing.dm b/code/modules/client/preference/link_processing.dm index 8b63541f4a7..25737056a9d 100644 --- a/code/modules/client/preference/link_processing.dm +++ b/code/modules/client/preference/link_processing.dm @@ -946,6 +946,10 @@ if("input_lists") toggles2 ^= PREFTOGGLE_2_DISABLE_TGUI_LISTS + if("tgui_say_light_mode") + toggles2 ^= PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE + user?.client?.tgui_say?.load() + if("ghost_att_anim") toggles2 ^= PREFTOGGLE_2_ITEMATTACK diff --git a/code/modules/client/preference/preferences.dm b/code/modules/client/preference/preferences.dm index e4678032a0f..ff1c8ce0bf5 100644 --- a/code/modules/client/preference/preferences.dm +++ b/code/modules/client/preference/preferences.dm @@ -439,7 +439,7 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts dat += "

Interface Settings

" dat += "Set screentip mode: [(screentip_mode == 0) ? "Disabled" : "[screentip_mode]px"]
" dat += "Screentip color:     Change
" - dat += "Thought Bubble: [(toggles2 & PREFTOGGLE_2_THOUGHT_BUBBLE) ? "Yes" : "No"]
" + dat += "Thought Bubble when pointing: [(toggles2 & PREFTOGGLE_2_THOUGHT_BUBBLE) ? "Yes" : "No"]
" dat += "Custom UI settings:
" dat += " - Alpha (transparency): [UI_style_alpha]
" dat += " - Color: [UI_style_color]    
" @@ -447,6 +447,7 @@ GLOBAL_LIST_INIT(special_role_times, list( //minimum age (in days) for accounts dat += "TGUI settings:
" dat += " - Fancy TGUI: [(toggles2 & PREFTOGGLE_2_FANCYUI) ? "Yes" : "No"]
" dat += " - Input Lists: [(toggles2 & PREFTOGGLE_2_DISABLE_TGUI_LISTS) ? "Default" : "TGUI"]
" + dat += " - TGUI Say Theme: [(toggles2 & PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE) ? "Light" : "Dark"]
" dat += "" if(TAB_ANTAG) // Antagonist's Preferences diff --git a/code/modules/client/preference/preferences_toggles.dm b/code/modules/client/preference/preferences_toggles.dm index 1e9129bc774..abe876bfa57 100644 --- a/code/modules/client/preference/preferences_toggles.dm +++ b/code/modules/client/preference/preferences_toggles.dm @@ -254,18 +254,6 @@ prefs.current_tab = 1 prefs.ShowChoices(usr) -/client/verb/toggle_popup_limiter() - set name = "Toggle Text Popup Limiter" - set category = "Preferences" - set desc = "Will let you limit the text input popups to one at a time." - prefs.toggles ^= PREFTOGGLE_TYPING_ONCE - prefs.save_preferences(src) - if(prefs.toggles & PREFTOGGLE_TYPING_ONCE) - to_chat(usr, "You have enabled text popup limiting.") - else - to_chat(usr, "You have disabled text popup limiting.") - return - /client/verb/toggle_ghost_pda() set name = "Show/Hide GhostPDA" set category = "Preferences" diff --git a/code/modules/mob/living/carbon/human/species/_species.dm b/code/modules/mob/living/carbon/human/species/_species.dm index 0ab16ec9953..d297ff26761 100644 --- a/code/modules/mob/living/carbon/human/species/_species.dm +++ b/code/modules/mob/living/carbon/human/species/_species.dm @@ -1031,6 +1031,7 @@ It'll return null if the organ doesn't correspond, so include null checks when u return /datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/organ/external/affecting, intent, mob/living/carbon/human/H) + return /proc/get_random_species(species_name = FALSE) // Returns a random non black-listed or hazardous species, either as a string or datum var/static/list/random_species = list() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index aee14dbb6f7..6474da71023 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1611,6 +1611,8 @@ GLOBAL_LIST_INIT(holy_areas, typecacheof(list( . = stat stat = new_stat SEND_SIGNAL(src, COMSIG_MOB_STATCHANGE, new_stat, .) + if(.) + set_typing_indicator(FALSE) ///Makes a call in the context of a different usr. Use sparingly /world/proc/invoke_callback_with_usr(mob/user_mob, datum/callback/invoked_callback, ...) diff --git a/code/modules/mob/mob_logout_base.dm b/code/modules/mob/mob_logout_base.dm index 6658adc78e1..59a13aacbeb 100644 --- a/code/modules/mob/mob_logout_base.dm +++ b/code/modules/mob/mob_logout_base.dm @@ -1,4 +1,5 @@ /mob/Logout() + set_typing_indicator(FALSE) SStgui.on_logout(src) // Cleanup any TGUIs the user has open unset_machine() GLOB.player_list -= src diff --git a/code/modules/mob/mob_vars.dm b/code/modules/mob/mob_vars.dm index f20d80240b2..cfd97814511 100644 --- a/code/modules/mob/mob_vars.dm +++ b/code/modules/mob/mob_vars.dm @@ -227,10 +227,10 @@ var/blood_state = BLOOD_STATE_NOT_BLOODY /// Assoc list for tracking how "bloody" a mobs feet are, used for creating bloody foot/shoeprints on turfs when moving var/list/bloody_feet = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_NOT_BLOODY = 0, BLOOD_BASE_ALPHA = BLOODY_FOOTPRINT_BASE_ALPHA) - /// set when typing in an input window instead of chatline, this var could probably be removed soon enough - var/hud_typing = 0 /// Affects if you have a typing indicator var/typing + /// Affects if you have a thinking indicator + var/thinking /// Last thing we typed in to the typing indicator, probably does not need to exist var/last_typed /// Last time we typed something in to the typing popup diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index f949dc33dd0..d69ed3d8880 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -1,6 +1,5 @@ -#define TYPING_INDICATOR_LIFETIME 30 * 10 //grace period after which typing indicator disappears regardless of text in chatbar - GLOBAL_LIST_EMPTY(typing_indicator) +GLOBAL_LIST_EMPTY(thinking_indicator) /** * Toggles the floating chat bubble above a players head. @@ -10,9 +9,8 @@ GLOBAL_LIST_EMPTY(typing_indicator) * * me - Is the bubble being caused by the 'me' emote command */ /mob/proc/set_typing_indicator(state, me) - if(!GLOB.typing_indicator[bubble_icon]) - GLOB.typing_indicator[bubble_icon] = image('icons/mob/talk.dmi', null, "[bubble_icon]typing", FLY_LAYER) + GLOB.typing_indicator[bubble_icon] = image('icons/mob/talk.dmi', null, "[bubble_icon]_typing", ABOVE_HUD_LAYER) var/image/I = GLOB.typing_indicator[bubble_icon] I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA @@ -20,91 +18,93 @@ GLOBAL_LIST_EMPTY(typing_indicator) var/mob/living/carbon/human/H = src if(HAS_TRAIT(H, TRAIT_MUTE)) overlays -= GLOB.typing_indicator[bubble_icon] - return + typing = FALSE + return FALSE - if(client) - if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING) || (me && (client.prefs.toggles2 & PREFTOGGLE_2_EMOTE_BUBBLE))) - overlays -= GLOB.typing_indicator[bubble_icon] - else - if(state) - if(!typing) - overlays += GLOB.typing_indicator[bubble_icon] - typing = TRUE - else - if(typing) - overlays -= GLOB.typing_indicator[bubble_icon] - typing = FALSE - return state + if(!client) + return FALSE + + if(stat != CONSCIOUS || is_muzzled() || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING)) + overlays -= GLOB.typing_indicator[bubble_icon] + typing = FALSE + return FALSE + + if(state && !typing) + overlays += GLOB.typing_indicator[bubble_icon] + typing = TRUE + + if(!state && typing) + overlays -= GLOB.typing_indicator[bubble_icon] + typing = FALSE + + return state + +/** + * Toggles the floating thought bubble above a players head. + * + * Arguments: + * * state - Should a thought bubble be shown or hidden + */ +/mob/proc/set_thinking_indicator(state) + if(!GLOB.thinking_indicator[bubble_icon]) + GLOB.thinking_indicator[bubble_icon] = image('icons/mob/talk.dmi', null, "[bubble_icon]_thinking", ABOVE_HUD_LAYER) + var/image/I = GLOB.thinking_indicator[bubble_icon] + I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA + + if(!client && !isliving(src)) + return FALSE + + if(stat != CONSCIOUS || (client.prefs.toggles & PREFTOGGLE_SHOW_TYPING)) + overlays -= GLOB.thinking_indicator[bubble_icon] + thinking = FALSE + return FALSE + + if(!state && thinking) + overlays -= GLOB.thinking_indicator[bubble_icon] + thinking = FALSE + + if(state && !thinking) + overlays += GLOB.thinking_indicator[bubble_icon] + thinking = TRUE + + return state /mob/verb/say_wrapper() set name = ".Say" - set hidden = 1 + set hidden = TRUE set_typing_indicator(TRUE) - hud_typing = 1 + typing = TRUE var/message = typing_input(src, "", "say (text)") - hud_typing = 0 + typing = FALSE set_typing_indicator(FALSE) if(message) say_verb(message) /mob/verb/me_wrapper() set name = ".Me" - set hidden = 1 - + set hidden = TRUE set_typing_indicator(TRUE, TRUE) - hud_typing = 1 + typing = TRUE var/message = typing_input(src, "", "me (text)") - hud_typing = 0 + typing = FALSE set_typing_indicator(FALSE) if(message) me_verb(message) -/mob/proc/handle_typing_indicator() - if(client) - if(!(client.prefs.toggles & PREFTOGGLE_SHOW_TYPING) && !hud_typing) - var/temp = winget(client, "input", "text") - - if(temp != last_typed) - last_typed = temp - last_typed_time = world.time - - if(world.time > last_typed_time + TYPING_INDICATOR_LIFETIME) - set_typing_indicator(FALSE) - return - if(length(temp) > 5 && findtext(temp, "Say \"", 1, 7)) - set_typing_indicator(TRUE) - else if(length(temp) > 3 && findtext(temp, "Me ", 1, 5)) - set_typing_indicator(TRUE, TRUE) - - else - set_typing_indicator(FALSE) - /client/verb/typing_indicator() set name = "Show/Hide Typing Indicator" set category = "Preferences" - set desc = "Toggles showing an indicator when you are typing a message." + set desc = "Toggles showing a typing/thought indicator when you have TGUIsay open." prefs.toggles ^= PREFTOGGLE_SHOW_TYPING prefs.save_preferences(src) - to_chat(src, "You will [(prefs.toggles & PREFTOGGLE_SHOW_TYPING) ? "no longer" : "now"] display a typing indicator.") + to_chat(src, "You will [(prefs.toggles & PREFTOGGLE_SHOW_TYPING) ? "no longer" : "now"] display a typing/thought indicator when you have TGUIsay open.") // Clear out any existing typing indicator. if(prefs.toggles & PREFTOGGLE_SHOW_TYPING) if(istype(mob)) mob.set_typing_indicator(FALSE) + mob.set_thinking_indicator(FALSE) SSblackbox.record_feedback("tally", "toggle_verbs", 1, "Toggle Typing Indicator (Speech)") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - - -/client/verb/emote_indicator() - set name = "Show/Hide Emote Typing Indicator" - set category = "Preferences" - set desc = "Toggles showing an indicator when you are typing an emote." - prefs.toggles2 ^= PREFTOGGLE_2_EMOTE_BUBBLE - prefs.save_preferences(src) - to_chat(src, "You will [(prefs.toggles2 & PREFTOGGLE_2_EMOTE_BUBBLE) ? "no longer" : "now"] display a typing indicator for emotes.") - - SSblackbox.record_feedback("tally", "toggle_verbs", 1, "Toggle Typing Indicator (Emote)") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - -#undef TYPING_INDICATOR_LIFETIME diff --git a/code/modules/tgui/tgui_datum.dm b/code/modules/tgui/tgui_datum.dm index da2c28736b1..1efece27cad 100644 --- a/code/modules/tgui/tgui_datum.dm +++ b/code/modules/tgui/tgui_datum.dm @@ -87,6 +87,7 @@ window.acquire_lock(src) if(!window.is_ready()) window.initialize( + strict_mode = TRUE, fancy = (user.client.prefs.toggles2 & PREFTOGGLE_2_FANCYUI), assets = list( get_asset_datum(/datum/asset/simple/tgui), diff --git a/code/modules/tgui/tgui_panel/tgui_panel.dm b/code/modules/tgui/tgui_panel/tgui_panel.dm index 53da137e89c..b76fa9f99de 100644 --- a/code/modules/tgui/tgui_panel/tgui_panel.dm +++ b/code/modules/tgui/tgui_panel/tgui_panel.dm @@ -13,9 +13,12 @@ var/broken = FALSE var/initialized_at -/datum/tgui_panel/New(client/client) +/datum/tgui_panel/New(client/client, id) + if(!id) + qdel(src) + CRASH("New TGUI panel created for [client] but no id supplied, deleting.") src.client = client - window = new(client, "browseroutput") + window = new(client, id) window.subscribe(src, PROC_REF(on_message)) /datum/tgui_panel/Del() @@ -42,9 +45,11 @@ sleep(1) initialized_at = world.time // Perform a clean initialization - window.initialize(assets = list( - get_asset_datum(/datum/asset/simple/tgui_panel), - )) + window.initialize( + strict_mode = TRUE, + assets = list( + get_asset_datum(/datum/asset/simple/tgui_panel), + )) window.send_asset(get_asset_datum(/datum/asset/simple/namespaced/fontawesome)) request_telemetry() addtimer(CALLBACK(src, PROC_REF(on_initialize_timed_out)), 5 SECONDS) diff --git a/code/modules/tgui/tgui_panel/tgui_panel_external.dm b/code/modules/tgui/tgui_panel/tgui_panel_external.dm index bdfc10b77ab..67cd6d612e2 100644 --- a/code/modules/tgui/tgui_panel/tgui_panel_external.dm +++ b/code/modules/tgui/tgui_panel/tgui_panel_external.dm @@ -34,3 +34,11 @@ // Force show the panel to see if there are any errors winset(src, "output", "is-disabled=1&is-visible=0") winset(src, "browseroutput", "is-disabled=0;is-visible=1") + +/client/verb/refresh_tgui() + set name = "Refresh TGUI" + set category = "Special Verbs" + + for(var/window_id in tgui_windows) + var/datum/tgui_window/window = tgui_windows[window_id] + window.reinitialize() diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm index e09e4ff9efc..0bdbc3778d6 100644 --- a/code/modules/tgui/tgui_window.dm +++ b/code/modules/tgui/tgui_window.dm @@ -18,6 +18,7 @@ var/message_queue var/sent_assets = list() // Vars passed to initialize proc (and saved for later) + var/initial_strict_mode var/initial_fancy var/initial_assets var/initial_inline_html @@ -47,11 +48,15 @@ * state. You can begin sending messages right after initializing. Messages * will be put into the queue until the window finishes loading. * - * optional assets list List of assets to inline into the html. - * optional inline_html string Custom HTML to inject. - * optional fancy bool If TRUE, will hide the window titlebar. + * optional strict_mode bool - Enables strict error handling and BSOD. + * optional fancy bool - If TRUE and if this is NOT a panel, will hide the window titlebar. + * optional assets list - List of assets to load during initialization. + * optional inline_html string - Custom HTML to inject. + * optional inline_js string - Custom JS to inject. + * optional inline_css string - Custom CSS to inject. */ /datum/tgui_window/proc/initialize( + strict_mode = FALSE, fancy = FALSE, assets = list(), inline_html = "", @@ -77,6 +82,7 @@ // Generate page html var/html = SStgui.basehtml html = replacetextEx(html, "\[tgui:windowId]", id) + html = replacetextEx(html, "\[tgui:strictMode]", strict_mode) // Inject assets var/inline_assets_str = "" for(var/datum/asset/asset in assets) @@ -94,14 +100,14 @@ html = replacetextEx(html, "\n", inline_assets_str) // Inject inline HTML if(inline_html) - html = replacetextEx(html, "", inline_html) + html = replacetextEx(html, "", isfile(inline_html) ? file2text(inline_html) : inline_html) // Inject inline JS if(inline_js) - inline_js = "" + inline_js = "" html = replacetextEx(html, "", inline_js) // Inject inline CSS if(inline_css) - inline_css = "" + inline_css = "" html = replacetextEx(html, "", inline_css) // Open the window client << browse(html, "window=[id];[options]") @@ -111,6 +117,23 @@ if(!is_browser) winset(client, id, "on-close=\"uiclose [id]\"") +/** + * public + * + * Reinitializes the panel with previous data used for initialization. + */ +/datum/tgui_window/proc/reinitialize() + initialize( + strict_mode = initial_strict_mode, + fancy = initial_fancy, + assets = initial_assets, + inline_html = initial_inline_html, + inline_js = initial_inline_js, + inline_css = initial_inline_css) + // Resend assets + for(var/datum/asset/asset in sent_assets) + send_asset(asset) + /** * public * @@ -287,6 +310,18 @@ : "[id].browser:update") message_queue = null +/** + * public + * + * Replaces the inline HTML content. + * + * required inline_html string HTML to inject + */ +/datum/tgui_window/proc/replace_html(inline_html = "") + client << output(url_encode(inline_html), is_browser \ + ? "[id]:replaceHtml" \ + : "[id].browser:replaceHtml") + /** * private * @@ -329,15 +364,6 @@ if("openLink") client << link(href_list["url"]) if("cacheReloaded") - // Reinitialize - initialize( - fancy = initial_fancy, - assets = initial_assets, - inline_html = initial_inline_html, - inline_js = initial_inline_js, - inline_css = initial_inline_css) - // Resend the assets - for(var/asset in sent_assets) - send_asset(asset) + reinitialize() if("chat/resend") SSchat.handle_resend(client, payload) diff --git a/code/modules/tgui_input/say_modal/tgui_say_modal.dm b/code/modules/tgui_input/say_modal/tgui_say_modal.dm new file mode 100644 index 00000000000..bf5c9621fbc --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_modal.dm @@ -0,0 +1,123 @@ +/** + * Creates a JSON encoded message to open TGUI say modals properly. + * + * Arguments: + * channel - The channel to open the modal in. + * Returns: + * string - A JSON encoded message to open the modal. + */ +/client/proc/tgui_say_create_open_command(channel) + var/message = TGUI_CREATE_MESSAGE("open", list( + channel = channel, + )) + return "\".output tgui_say.browser:update [message]\"" + +/** + * The tgui say modal. This initializes an input window which hides until + * the user presses one of the speech hotkeys. Once something is entered, it will + * delegate the speech to the proper channel. + */ +/datum/tgui_say + /// The user who opened the window + var/client/client + /// The modal window + var/datum/tgui_window/window + /// Boolean for whether the tgui_say was opened by the user. + var/window_open + +/** Creates the new input window to exist in the background. */ +/datum/tgui_say/New(client/client, id) + src.client = client + window = new(client, id) + winset(client, "tgui_say", "size=1,1;is-visible=0;") + window.subscribe(src, PROC_REF(on_message)) + window.is_browser = TRUE + +/** + * After a brief period, injects the scripts into + * the window to listen for open commands. + */ +/datum/tgui_say/proc/initialize() + set waitfor = FALSE + // Sleep to defer initialization to after client constructor + sleep(3) + window.initialize( + strict_mode = TRUE, + fancy = TRUE, + inline_css = file("tgui/public/tgui-say.bundle.css"), + inline_js = file("tgui/public/tgui-say.bundle.js"), + ); + +/** + * Ensures nothing funny is going on window load. + * Minimizes the window, sets max length, closes all + * typing and thinking indicators. This is triggered + * as soon as the window sends the "ready" message. + */ +/datum/tgui_say/proc/load() + window_open = FALSE + winset(client, "tgui_say", "pos=848,500;size=275,30;is-visible=0;") + window.send_message("props", list( + "lightMode" = (client.prefs.toggles2 & PREFTOGGLE_2_ENABLE_TGUI_SAY_LIGHT_MODE), + "maxLength" = MAX_MESSAGE_LEN, + )) + stop_thinking() + return TRUE + +/** + * Sets the window as "opened" server side, though it is already + * visible to the user. We do this to set local vars & + * start typing (if enabled and in an IC channel). + * + * Arguments: + * payload - A list containing the channel the window was opened in. + */ +/datum/tgui_say/proc/open(payload) + if(!payload?["channel"]) + CRASH("No channel provided to an open TGUI-Say") + window_open = TRUE + switch(payload["channel"]) + if(ME_CHANNEL, RADIO_CHANNEL, SAY_CHANNEL, WHISPER_CHANNEL) + start_thinking() + return TRUE + +/** + * Closes the window serverside. Closes any open chat bubbles + * regardless of preference. + */ +/datum/tgui_say/proc/close() + window_open = FALSE + stop_thinking() + stop_typing() + +/** + * The equivalent of ui_act, this waits on messages from the window + * and delegates actions. + */ +/datum/tgui_say/proc/on_message(type, payload) + switch(type) + if("ready") + load() + return TRUE + if("open") + open(payload) + return TRUE + if("close") + close() + return TRUE + if("thinking") + if(payload["visible"] == TRUE) + start_thinking() + return TRUE + if(payload["visible"] == FALSE) + stop_thinking() + return TRUE + return FALSE + if("typing") + start_typing(payload["isMeChannel"]) + return TRUE + if("entry") + handle_entry(payload) + return TRUE + + return FALSE diff --git a/code/modules/tgui_input/say_modal/tgui_say_speech.dm b/code/modules/tgui_input/say_modal/tgui_say_speech.dm new file mode 100644 index 00000000000..b7c9a48a065 --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_speech.dm @@ -0,0 +1,61 @@ +/** + * Delegates the speech to the proper channel. + * + * Arguments: + * entry - the text to broadcast + * channel - the channel to broadcast in + * Returns: + * boolean - on success or failure + */ +/datum/tgui_say/proc/delegate_speech(entry, channel) + switch(channel) + if(SAY_CHANNEL) + client.mob.say_verb(entry) + return TRUE + if(RADIO_CHANNEL) + client.mob.say_verb((isliving(client.mob) ? ";" : "") + entry) + return TRUE + if(WHISPER_CHANNEL) + client.mob.whisper(entry) + return TRUE + if(ME_CHANNEL) + client.mob.me_verb(entry) + return TRUE + if(OOC_CHANNEL) + client.ooc(entry) + return TRUE + if(LOOC_CHANNEL) + client.looc(entry) + return TRUE + if(ADMIN_CHANNEL) + client.cmd_admin_say(entry) + return TRUE + if(MENTOR_CHANNEL) + client.cmd_mentor_say(entry) + return TRUE + if(DSAY_CHANNEL) + client.dsay(entry) + return TRUE + return FALSE + +/** + * Handles text entry and forced speech. + * + * Arguments: + * payload - a string list containing entry & channel + * Returns: + * boolean - success or failure + */ +/datum/tgui_say/proc/handle_entry(payload) + if(!payload?["channel"] || !payload["entry"]) + var/hacker_man_ckey = usr.client.ckey + qdel(usr.client) + message_admins("[hacker_man_ckey] was kicked for attemping to send a null message to TGUI-say.") + CRASH("[hacker_man_ckey] entered in a null payload to the chat window.") + if(length(payload["entry"]) > MAX_MESSAGE_LEN) + var/hacker_man_ckey = usr.client.ckey + qdel(usr.client) + message_admins("[hacker_man_ckey] was kicked for attemping to bypass TGUI-say character limits.") + CRASH("[hacker_man_ckey] has entered more characters than allowed into a TGUI-Say.") + delegate_speech(payload["entry"], payload["channel"]) + return TRUE diff --git a/code/modules/tgui_input/say_modal/tgui_say_typing.dm b/code/modules/tgui_input/say_modal/tgui_say_typing.dm new file mode 100644 index 00000000000..9c137ec5ad9 --- /dev/null +++ b/code/modules/tgui_input/say_modal/tgui_say_typing.dm @@ -0,0 +1,41 @@ +/** Sets the mob as "thinking" - with indicator */ +/datum/tgui_say/proc/start_thinking() + if(!client?.mob || !window_open) + return FALSE + /// Special exemptions + if(isabductor(client.mob)) + return FALSE + client.mob.set_thinking_indicator(TRUE) + addtimer(CALLBACK(src, PROC_REF(stop_thinking)), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) + +/** Removes typing/thinking indicators and flags the mob as not thinking */ +/datum/tgui_say/proc/stop_thinking() + if(!client?.mob) + return FALSE + client.mob.set_thinking_indicator(FALSE) + +/** + * Handles the user typing. After a brief period of inactivity, + * signals the client mob to revert to the "thinking" icon. + */ +/datum/tgui_say/proc/start_typing(me = FALSE) + if(!client?.mob) + return FALSE + client.mob.set_typing_indicator(FALSE) + client.mob.set_thinking_indicator(FALSE) + if(!window_open) + return FALSE + client.mob.set_typing_indicator(TRUE, me) + addtimer(CALLBACK(src, PROC_REF(stop_typing)), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) + +/** + * Callback to remove the typing indicator after a brief period of inactivity. + * If the user was typing IC, the thinking indicator is shown. + */ +/datum/tgui_say/proc/stop_typing() + if(!client?.mob) + return FALSE + client.mob.set_typing_indicator(FALSE) + if(!window_open) + return FALSE + client.mob.set_thinking_indicator(TRUE) diff --git a/icons/mob/talk.dmi b/icons/mob/talk.dmi index 961cffec7f6..08c2c06f7c5 100644 Binary files a/icons/mob/talk.dmi and b/icons/mob/talk.dmi differ diff --git a/interface/skin.dmf b/interface/skin.dmf index b21716cc0f8..f8e3568852a 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -375,3 +375,22 @@ window "infowindow" is-default = true highlight-color = #00aa00 on-show = ".winset \"rpane.infob.is-checked=true?rpane.rpanewindow.top=infowindow:rpane.rpanewindow.top=\"" + +window "tgui_say" + elem "tgui_say" + type = MAIN + pos = 848,500 + size = 275x30 + anchor1 = 50,50 + anchor2 = 50,50 + is-visible = false + saved-params = "" + statusbar = false + can-minimize = false + elem "browser" + type = BROWSER + pos = 0,0 + size = 275x30 + anchor1 = 0,0 + anchor2 = 0,0 + saved-params = "" diff --git a/paradise.dme b/paradise.dme index 869b5150483..d326f5d9fb7 100644 --- a/paradise.dme +++ b/paradise.dme @@ -97,6 +97,7 @@ #include "code\__DEFINES\shuttle_defines.dm" #include "code\__DEFINES\sight.dm" #include "code\__DEFINES\sound_defines.dm" +#include "code\__DEFINES\speech_channels.dm" #include "code\__DEFINES\spell_defines.dm" #include "code\__DEFINES\stat.dm" #include "code\__DEFINES\station_defines.dm" @@ -478,6 +479,7 @@ #include "code\datums\keybindings\ai_keybinds.dm" #include "code\datums\keybindings\carbon_keybinds.dm" #include "code\datums\keybindings\client.dm" +#include "code\datums\keybindings\communication_keybinds.dm" #include "code\datums\keybindings\emote_keybinds.dm" #include "code\datums\keybindings\human_keybinds.dm" #include "code\datums\keybindings\living_keybinds.dm" @@ -2809,6 +2811,9 @@ #include "code\modules\tgui\tgui_panel\tgui_panel_external.dm" #include "code\modules\tgui\tgui_panel\tgui_panel_message.dm" #include "code\modules\tgui\tgui_panel\to_chat.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_modal.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_speech.dm" +#include "code\modules\tgui_input\say_modal\tgui_say_typing.dm" #include "code\modules\tooltip\tooltip.dm" #include "code\modules\unit_tests\_unit_tests.dm" #include "code\modules\vehicle\ambulance.dm" diff --git a/tgui/packages/common/keys.ts b/tgui/packages/common/keys.ts new file mode 100644 index 00000000000..61b79992b48 --- /dev/null +++ b/tgui/packages/common/keys.ts @@ -0,0 +1,39 @@ +/** + * ### Key codes. + * event.keyCode is deprecated, use this reference instead. + * + * Handles modifier keys (Shift, Alt, Control) and arrow keys. + * + * For alphabetical keys, use the actual character (e.g. 'a') instead of the key code. + * + * Something isn't here that you want? Just add it: + * @url https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + * @usage + * ```ts + * import { KEY } from 'tgui/common/keys'; + * + * if (event.key === KEY.Enter) { + * // do something + * } + * ``` + */ +export enum KEY { + Alt = 'Alt', + Backspace = 'Backspace', + Control = 'Control', + Delete = 'Delete', + Down = 'Down', + End = 'End', + Enter = 'Enter', + Escape = 'Esc', + Home = 'Home', + Insert = 'Insert', + Left = 'Left', + PageDown = 'PageDown', + PageUp = 'PageUp', + Right = 'Right', + Shift = 'Shift', + Space = ' ', + Tab = 'Tab', + Up = 'Up', +} diff --git a/tgui/packages/common/timer.js b/tgui/packages/common/timer.js deleted file mode 100644 index 0b1c68199e9..00000000000 --- a/tgui/packages/common/timer.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @file - * @copyright 2020 Aleksej Komarov - * @license MIT - */ - -/** - * Returns a function, that, as long as it continues to be invoked, will - * not be triggered. The function will be called after it stops being - * called for N milliseconds. If `immediate` is passed, trigger the - * function on the leading edge, instead of the trailing. - */ -export const debounce = (fn, time, immediate = false) => { - let timeout; - return (...args) => { - const later = () => { - timeout = null; - if (!immediate) { - fn(...args); - } - }; - const callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, time); - if (callNow) { - fn(...args); - } - }; -}; - -/** - * Suspends an asynchronous function for N milliseconds. - * - * @param {number} time - */ -export const sleep = (time) => - new Promise((resolve) => setTimeout(resolve, time)); diff --git a/tgui/packages/common/timer.ts b/tgui/packages/common/timer.ts new file mode 100644 index 00000000000..21c43311010 --- /dev/null +++ b/tgui/packages/common/timer.ts @@ -0,0 +1,68 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +/** + * Returns a function, that, as long as it continues to be invoked, will + * not be triggered. The function will be called after it stops being + * called for N milliseconds. If `immediate` is passed, trigger the + * function on the leading edge, instead of the trailing. + */ +export const debounce = any>( + fn: F, + time: number, + immediate = false +): ((...args: Parameters) => void) => { + let timeout: ReturnType | null; + return (...args: Parameters) => { + const later = () => { + timeout = null; + if (!immediate) { + fn(...args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout!); + timeout = setTimeout(later, time); + if (callNow) { + fn(...args); + } + }; +}; + +/** + * Suspends an asynchronous function for N milliseconds. + * + * @param {number} time + */ +export const sleep = (time: number): Promise => + new Promise((resolve) => setTimeout(resolve, time)); + +/** + * Returns a function, that, when invoked, will only be triggered at most once + * during a given window of time. + */ +export const throttle = any>( + fn: F, + time: number +): ((...args: Parameters) => void) => { + let previouslyRun: number | null, + queuedToRun: ReturnType | null; + return function invokeFn(...args: Parameters) { + const now = Date.now(); + if (queuedToRun) { + clearTimeout(queuedToRun); + } + if (!previouslyRun || now - previouslyRun >= time) { + fn.apply(null, args); + previouslyRun = now; + } else { + queuedToRun = setTimeout( + () => invokeFn(...args), + time - (now - (previouslyRun ?? 0)) + ); + } + }; +}; diff --git a/tgui/packages/tgui-say/ChannelIterator.test.ts b/tgui/packages/tgui-say/ChannelIterator.test.ts new file mode 100644 index 00000000000..49c96d3e403 --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.test.ts @@ -0,0 +1,49 @@ +import { ChannelIterator } from './ChannelIterator'; + +describe('ChannelIterator', () => { + let channelIterator: ChannelIterator; + + beforeEach(() => { + channelIterator = new ChannelIterator(); + }); + + it('should cycle through channels properly', () => { + expect(channelIterator.current()).toBe('Say'); + expect(channelIterator.next()).toBe('Radio'); + expect(channelIterator.next()).toBe('Whisper'); + expect(channelIterator.next()).toBe('Me'); + expect(channelIterator.next()).toBe('OOC'); + expect(channelIterator.next()).toBe('LOOC'); + expect(channelIterator.next()).toBe('Say'); // Admin, Mentor, and Dsay are blacklisted so should be skipped + }); + + it('should set a channel properly', () => { + channelIterator.set('OOC'); + expect(channelIterator.current()).toBe('OOC'); + }); + + it('should return true when current channel is "Say"', () => { + channelIterator.set('Say'); + expect(channelIterator.isSay()).toBe(true); + }); + + it('should return false when current channel is not "Say"', () => { + channelIterator.set('Radio'); + expect(channelIterator.isSay()).toBe(false); + }); + + it('should return true when current channel is visible', () => { + channelIterator.set('Say'); + expect(channelIterator.isVisible()).toBe(true); + }); + + it('should return false when current channel is not visible', () => { + channelIterator.set('OOC'); + expect(channelIterator.isVisible()).toBe(false); + }); + + it('should not leak a message from a blacklisted channel', () => { + channelIterator.set('Admin'); + expect(channelIterator.next()).toBe('Admin'); + }); +}); diff --git a/tgui/packages/tgui-say/ChannelIterator.ts b/tgui/packages/tgui-say/ChannelIterator.ts new file mode 100644 index 00000000000..088949ca3a1 --- /dev/null +++ b/tgui/packages/tgui-say/ChannelIterator.ts @@ -0,0 +1,83 @@ +export type Channel = + | 'Say' + | 'Radio' + | 'Whisper' + | 'Me' + | 'OOC' + | 'LOOC' + | 'Mentor' + | 'Admin' + | 'Dsay'; + +/** + * ### ChannelIterator + * Cycles a predefined list of channels, + * skipping over blacklisted ones, + * and providing methods to manage and query the current channel. + */ +export class ChannelIterator { + private index: number = 0; + private readonly channels: Channel[] = [ + 'Say', + 'Radio', + 'Whisper', + 'Me', + 'OOC', + 'LOOC', + 'Mentor', + 'Admin', + 'Dsay', + ]; + private readonly blacklist: Channel[] = ['Mentor', 'Admin', 'Dsay']; + private readonly quiet: Channel[] = [ + 'OOC', + 'LOOC', + 'Mentor', + 'Admin', + 'Dsay', + ]; + + public next(): Channel { + if (this.blacklist.includes(this.channels[this.index])) { + return this.channels[this.index]; + } + + for (let index = 1; index <= this.channels.length; index++) { + let nextIndex = (this.index + index) % this.channels.length; + if (!this.blacklist.includes(this.channels[nextIndex])) { + this.index = nextIndex; + break; + } + } + + return this.channels[this.index]; + } + + public isCurrentChannelBlacklisted(): boolean { + return this.blacklist.includes(this.channels[this.index]); + } + + public set(channel: Channel): void { + this.index = this.channels.indexOf(channel) || 0; + } + + public current(): Channel { + return this.channels[this.index]; + } + + public isMe(): boolean { + return this.channels[this.index] === 'Me'; + } + + public isSay(): boolean { + return this.channels[this.index] === 'Say'; + } + + public isVisible(): boolean { + return !this.quiet.includes(this.channels[this.index]); + } + + public reset(): void { + this.index = 0; + } +} diff --git a/tgui/packages/tgui-say/ChatHistory.test.ts b/tgui/packages/tgui-say/ChatHistory.test.ts new file mode 100644 index 00000000000..c6d8c1c2e27 --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.test.ts @@ -0,0 +1,50 @@ +import { ChatHistory } from './ChatHistory'; + +describe('ChatHistory', () => { + let chatHistory: ChatHistory; + + beforeEach(() => { + chatHistory = new ChatHistory(); + }); + + it('should add a message to the history', () => { + chatHistory.add('Hello'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); + + it('should retrieve older and newer messages', () => { + chatHistory.add('Hello'); + chatHistory.add('World'); + expect(chatHistory.getOlderMessage()).toEqual('World'); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + expect(chatHistory.getNewerMessage()).toEqual('World'); + expect(chatHistory.getNewerMessage()).toBeNull(); + expect(chatHistory.getOlderMessage()).toEqual('World'); + }); + + it('should limit the history to 5 messages', () => { + for (let i = 1; i <= 6; i++) { + chatHistory.add(`Message ${i}`); + } + + expect(chatHistory.getOlderMessage()).toEqual('Message 6'); + for (let i = 5; i >= 2; i--) { + expect(chatHistory.getOlderMessage()).toEqual(`Message ${i}`); + } + expect(chatHistory.getOlderMessage()).toBeNull(); + }); + + it('should handle temp message correctly', () => { + chatHistory.saveTemp('Temp message'); + expect(chatHistory.getTemp()).toEqual('Temp message'); + expect(chatHistory.getTemp()).toBeNull(); + }); + + it('should reset correctly', () => { + chatHistory.add('Hello'); + chatHistory.getOlderMessage(); + chatHistory.reset(); + expect(chatHistory.isAtLatest()).toBe(true); + expect(chatHistory.getOlderMessage()).toEqual('Hello'); + }); +}); diff --git a/tgui/packages/tgui-say/ChatHistory.ts b/tgui/packages/tgui-say/ChatHistory.ts new file mode 100644 index 00000000000..b5490b1887f --- /dev/null +++ b/tgui/packages/tgui-say/ChatHistory.ts @@ -0,0 +1,59 @@ +/** + * ### ChatHistory + * A class to manage a chat history, + * maintaining a maximum of five messages and supporting navigation, + * temporary message storage, and query operations. + */ +export class ChatHistory { + private messages: string[] = []; + private index: number = -1; // Initialize index at -1 + private temp: string | null = null; + + public add(message: string): void { + this.messages.unshift(message); + this.index = -1; // Reset index + if (this.messages.length > 5) { + this.messages.pop(); + } + } + + public getIndex(): number { + return this.index + 1; + } + + public getOlderMessage(): string | null { + if (this.messages.length === 0 || this.index >= this.messages.length - 1) { + return null; + } + this.index++; + return this.messages[this.index]; + } + + public getNewerMessage(): string | null { + if (this.index <= 0) { + this.index = -1; + return null; + } + this.index--; + return this.messages[this.index]; + } + + public isAtLatest(): boolean { + return this.index === -1; + } + + public saveTemp(message: string): void { + this.temp = message; + } + + public getTemp(): string | null { + const temp = this.temp; + this.temp = null; + return temp; + } + + public reset(): void { + this.index = -1; + this.temp = null; + } +} diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx new file mode 100644 index 00000000000..c810aa0a084 --- /dev/null +++ b/tgui/packages/tgui-say/TguiSay.tsx @@ -0,0 +1,380 @@ +import { Channel, ChannelIterator } from './ChannelIterator'; +import { ChatHistory } from './ChatHistory'; +import { Component, createRef, InfernoKeyboardEvent, RefObject } from 'inferno'; +import { LINE_LENGTHS, RADIO_PREFIXES, WINDOW_SIZES } from './constants'; +import { byondMessages } from './timers'; +import { dragStartHandler } from 'tgui/drag'; +import { windowOpen, windowClose, windowSet } from './helpers'; +import { BooleanLike } from 'common/react'; +import { KEY } from 'common/keys'; + +type ByondOpen = { + channel: Channel; +}; + +type ByondProps = { + maxLength: number; + lightMode: BooleanLike; +}; + +type State = { + buttonContent: string | number; + size: WINDOW_SIZES; +}; + +// Picks out radio channel keycodes +const CHANNEL_REGEX = /^[:#.][^\s]\s/; + +export class TguiSay extends Component<{}, State> { + private channelIterator: ChannelIterator; + private chatHistory: ChatHistory; + private currentPrefix: keyof typeof RADIO_PREFIXES | null; + private innerRef: RefObject; + private lightMode: boolean; + private maxLength: number; + private messages: typeof byondMessages; + // eslint-disable-next-line react/state-in-constructor + state: State; + + constructor(props: never) { + super(props); + + this.channelIterator = new ChannelIterator(); + this.chatHistory = new ChatHistory(); + this.currentPrefix = null; + this.innerRef = createRef(); + this.lightMode = false; + this.maxLength = 1024; + this.messages = byondMessages; + this.state = { + buttonContent: '', + size: WINDOW_SIZES.small, + }; + + this.handleArrowKeys = this.handleArrowKeys.bind(this); + this.handleBackspaceDelete = this.handleBackspaceDelete.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleEnter = this.handleEnter.bind(this); + this.handleIncrementChannel = this.handleIncrementChannel.bind(this); + this.handleInput = this.handleInput.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleProps = this.handleProps.bind(this); + this.reset = this.reset.bind(this); + this.setSize = this.setSize.bind(this); + this.setValue = this.setValue.bind(this); + this.handleButtonClick = this.handleButtonClick.bind(this); + } + + componentDidMount() { + Byond.subscribeTo('props', this.handleProps); + Byond.subscribeTo('open', this.handleOpen); + } + + handleArrowKeys(direction: KEY.Up | KEY.Down) { + const currentValue = this.innerRef.current?.value; + + if (direction === KEY.Up) { + if (this.chatHistory.isAtLatest() && currentValue) { + // Save current message to temp history if at the most recent message + this.chatHistory.saveTemp(currentValue); + } + // Try to get the previous message, fall back to the current value if none + const prevMessage = this.chatHistory.getOlderMessage(); + + if (prevMessage) { + this.setState({ buttonContent: this.chatHistory.getIndex() }); + this.setSize(prevMessage.length); + this.setValue(prevMessage); + } + } else { + const nextMessage = + this.chatHistory.getNewerMessage() || this.chatHistory.getTemp() || ''; + + const buttonContent = this.chatHistory.isAtLatest() + ? this.channelIterator.current() + : this.chatHistory.getIndex(); + + this.setState({ buttonContent }); + this.setSize(nextMessage.length); + this.setValue(nextMessage); + } + } + + handleBackspaceDelete() { + const typed = this.innerRef.current?.value; + + // User is on a chat history message + if (!this.chatHistory.isAtLatest()) { + this.chatHistory.reset(); + this.setState({ + buttonContent: this.currentPrefix ?? this.channelIterator.current(), + }); + // Empty input, resets the channel + } else if ( + !!this.currentPrefix && + this.channelIterator.isSay() && + typed?.length === 0 + ) { + this.currentPrefix = null; + this.setState({ buttonContent: this.channelIterator.current() }); + } else if ( + this.innerRef.current?.selectionStart === 0 && + this.innerRef.current?.selectionEnd === 0 && + !this.channelIterator.isCurrentChannelBlacklisted() + ) { + this.currentPrefix = null; + this.channelIterator.set('Say'); + this.setState({ buttonContent: this.channelIterator.current() }); + } + + this.setSize(typed?.length); + } + + handleClose() { + const current = this.innerRef.current; + + if (current) { + current.blur(); + } + + this.reset(); + this.chatHistory.reset(); + this.channelIterator.reset(); + this.currentPrefix = null; + windowClose(); + } + + handleEnter() { + const prefix = this.currentPrefix ?? ''; + const value = this.innerRef.current?.value; + + if (value?.length && value.length < this.maxLength) { + this.chatHistory.add(value); + Byond.sendMessage('entry', { + channel: this.channelIterator.current(), + entry: this.channelIterator.isSay() ? prefix + value : value, + }); + } + + this.handleClose(); + } + + handleIncrementChannel() { + // Binary talk is a special case, tell byond to show thinking indicators + if ( + this.channelIterator.isSay() && + this.currentPrefix === (':b ' || '.b ' || '#b ') + ) { + this.messages.channelIncrementMsg(true); + } + + this.currentPrefix = null; + + this.channelIterator.next(); + + // If we've looped onto a quiet channel, tell byond to hide thinking indicators + if (!this.channelIterator.isVisible()) { + this.messages.channelIncrementMsg(false); + } + + this.setState({ buttonContent: this.channelIterator.current() }); + } + + // Throw focus back on the text area while executing button function as expecrted + handleButtonClick(selectionStart: number, selectionEnd: number) { + this.handleIncrementChannel(); + const textArea = this.innerRef?.current; + if (textArea) { + textArea.focus(); + textArea.setSelectionRange(selectionStart, selectionEnd); + } + } + + handleInput() { + const typed = this.innerRef.current?.value; + + // If we're typing, send the message + if ( + this.channelIterator.isVisible() && + this.currentPrefix !== (':b ' || '.b ' || '#b ') + ) { + this.messages.typingMsg(this.channelIterator.isMe()); + } + + this.setSize(typed?.length); + + // Early check for standard radio channel key + if (typed && typed.slice(0, 2) === '; ') { + this.channelIterator.set('Radio'); + this.currentPrefix = null; + this.setValue(typed.slice(2)); + this.setState({ buttonContent: this.channelIterator.current() }); + return; + } + + // Is there a value? Is it long enough to be a prefix? + if (!typed || typed.length < 3) { + return; + } + + if (!CHANNEL_REGEX.test(typed)) { + return; + } + + // Is it a valid prefix? + const prefix = typed + .slice(0, 3) + ?.toLowerCase() as keyof typeof RADIO_PREFIXES; + if (!RADIO_PREFIXES[prefix] || prefix === this.currentPrefix) { + return; + } + + // If we're in binary, hide the thinking indicator + if (prefix === (':b ' || '.b ' || '#b ')) { + Byond.sendMessage('thinking', { visible: false }); + } + + this.channelIterator.set('Say'); + this.currentPrefix = prefix; + this.setState({ buttonContent: RADIO_PREFIXES[prefix] }); + this.setValue(typed.slice(3)); + } + + handleKeyDown(event: InfernoKeyboardEvent) { + switch (event.key) { + case KEY.Up: + case KEY.Down: + event.preventDefault(); + this.handleArrowKeys(event.key); + break; + + case KEY.Delete: + case KEY.Backspace: + this.handleBackspaceDelete(); + break; + + case KEY.Enter: + event.preventDefault(); + this.handleEnter(); + break; + + case KEY.Tab: + event.preventDefault(); + this.handleIncrementChannel(); + break; + + case KEY.Escape: + this.handleClose(); + break; + } + } + + handleOpen = (data: ByondOpen) => { + setTimeout(() => { + this.innerRef.current?.focus(); + }, 0); + + const { channel } = data; + // Catches the case where the modal is already open + if (this.channelIterator.isSay()) { + this.channelIterator.set(channel); + } + this.setState({ buttonContent: this.channelIterator.current() }); + + windowOpen(this.channelIterator.current()); + }; + + handleProps = (data: ByondProps) => { + const { maxLength, lightMode } = data; + this.maxLength = maxLength; + this.lightMode = !!lightMode; + }; + + reset() { + this.setValue(''); + this.setSize(); + this.setState({ + buttonContent: this.channelIterator.current(), + }); + } + + setSize(length = 0) { + let newSize: WINDOW_SIZES; + + if (length > LINE_LENGTHS.medium) { + newSize = WINDOW_SIZES.large; + } else if (length <= LINE_LENGTHS.medium && length > LINE_LENGTHS.small) { + newSize = WINDOW_SIZES.medium; + } else { + newSize = WINDOW_SIZES.small; + } + + if (this.state.size !== newSize) { + this.setState({ size: newSize }); + windowSet(newSize); + } + } + + setValue(value: string) { + const textArea = this.innerRef.current; + if (textArea) { + textArea.value = value; + } + } + + render() { + const theme = + (this.lightMode && 'lightMode') || + (this.currentPrefix && RADIO_PREFIXES[this.currentPrefix]) || + this.channelIterator.current(); + + return ( +
+ +
+ +
+ +