From ebbc45b1616c08d2dc0b6e6ce48258f68eefd46a Mon Sep 17 00:00:00 2001 From: distributivgesetz Date: Thu, 3 Aug 2023 23:43:31 +0200 Subject: [PATCH] Improved PDA Direct Messenger (#75820) ## About The Pull Request Fixes #76708, Closes #76729 (sorry Zephyr) This PR expands the Direct Messenger UI, adding a chat screen for each available messenger that you can find, and moving message sending over to TGUI. This chat screen includes a message log that displays messages sent by you as well as messages received from the recipient. This gets rid of the previous chat log, which just had all messages thrown together that you received or have sent, in one big list. Furthermore, all messaging is now done inside the UI. This kills all TGUI popups you would ever need to send messages forever (except for quick replies). Use the input bar on the bottom, press Enter or the Send button, and it sends your message. Spam mode is now done in the UI too, via a text field you can find in the contacts list. Additionally, because I have a habit of blowing things massively out of scope, I've also completely refactored how messages and chat logs are stored in the PDA messenger. I plan on using this in a PR that merges the chat client with the messenger, sometime in the future. Sorry this took so long. Stuff left to do before I open this PR for review: - [x] Add "recent messages" - [x] Add "unread messages" - [x] Add message drafts - [x] Make photo sending not shit - [x] Implement the edge cases for automated and rigged messages - [x] Make sure shit isn't fucked - [x] Profit
Screenshots ![dreamseeker_HIrEfrap5X](https://github.com/tgstation/tgstation/assets/47710522/97c713b7-dda3-44d3-a8f5-d0ec11c92668) ![qIOWhVld4l](https://github.com/tgstation/tgstation/assets/47710522/3ab4e2c1-a38f-4b20-8e9f-509ea14c0434) ![dreamseeker_LIqwi05i4O](https://github.com/tgstation/tgstation/assets/47710522/c051c791-b595-4166-a4d3-82cb7568411f) ![BIYxNVjGL7](https://github.com/tgstation/tgstation/assets/47710522/b9c97eab-52b5-449f-b00f-a0d8aa5f865c) ![dreamseeker_IWdoSsUinC](https://github.com/tgstation/tgstation/assets/47710522/2a4cd76a-2bdc-4283-b642-09e92476fef5) ![L9DxzFHDEF](https://github.com/tgstation/tgstation/assets/47710522/6a5b0e29-d535-4c7e-a88e-e9b71198719b) ![rAuDgqBLNE](https://github.com/tgstation/tgstation/assets/47710522/128a0291-91da-4f9e-9bc5-a65cf411ea6d) ![dreamseeker_voui6S8MUf](https://github.com/tgstation/tgstation/assets/47710522/6e3ba044-b8df-492d-b58d-6c73ab07233d) ![image](https://github.com/tgstation/tgstation/assets/47710522/522c1d85-b9cf-4e0e-9588-9d3993eea03f)
## Why It's Good For The Game The UI has largely stayed the same since modular tablets were added a year ago. Even better, direct messaging has been the same since PDAs were first added *more than a decade ago*. Imagine that. Now we finally actually (!) make use of those brand new features that we got from the TGUI switch in this regard. ## Changelog :cl: distributivgesetz add: Updated Direct Messenger to v6.5.3. Now including brand new individual chat rooms, proper image attachments and a revolutionary message input field! add: Added a "Reset Imprint" option to the PDA painter. refactor: Refactored PDA imprinting code just a bit. fix: PDAs should now properly respond to rigged messages. /:cl: --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> --- code/__DEFINES/dcs/signals/signals_datum.dm | 6 - .../dcs/signals/signals_modular_computer.dm | 15 + code/__HELPERS/chat_filter.dm | 55 +- code/__HELPERS/cmp.dm | 8 +- .../subsystem/modular_computers.dm | 10 + code/datums/records/crime.dm | 17 +- code/game/machinery/PDApainter.dm | 6 + .../machinery/telecomms/computers/message.dm | 23 +- .../telecomms/machines/broadcaster.dm | 6 +- .../telecomms/machines/message_server.dm | 61 +- .../game/objects/items/devices/radio/radio.dm | 7 +- .../objects/items/devices/traitordevices.dm | 6 + code/modules/admin/admin_pda_message.dm | 74 +- .../chameleon/chameleon_action_subtypes.dm | 2 +- code/modules/clothing/outfits/ert.dm | 3 +- code/modules/clothing/outfits/standard.dm | 3 +- code/modules/jobs/job_types/_job.dm | 3 +- .../jobs/job_types/security_officer.dm | 14 +- code/modules/mob/living/silicon/ai/ai.dm | 2 +- .../modules/mob/living/silicon/robot/robot.dm | 2 +- .../mob/living/silicon/robot/robot_defines.dm | 3 +- code/modules/mob/living/silicon/silicon.dm | 11 +- code/modules/mob/mob.dm | 2 +- .../computers/item/computer.dm | 37 +- .../computers/item/computer_files.dm | 38 +- .../computers/item/computer_ui.dm | 5 +- .../computers/item/disks/virus_disk.dm | 17 +- .../computers/item/role_tablet_presets.dm | 4 +- .../file_system/computer_file.dm | 2 +- .../file_system/picture_file.dm | 37 + .../modular_computers/file_system/program.dm | 8 + .../file_system/programs/frontier.dm | 8 +- .../programs/messenger/messenger_data.dm | 152 ++++ .../programs/messenger/messenger_program.dm | 731 ++++++++++++++++++ .../file_system/programs/ntmessenger.dm | 426 ---------- .../file_system/programs/wirecarp.dm | 21 +- tgstation.dme | 5 +- tgui/packages/tgui/interfaces/AdminPDA.js | 72 +- .../packages/tgui/interfaces/NtosMessenger.js | 240 ------ .../interfaces/NtosMessenger/ChatScreen.tsx | 430 +++++++++++ .../tgui/interfaces/NtosMessenger/index.tsx | 336 ++++++++ .../tgui/interfaces/NtosMessenger/types.tsx | 31 + .../tgui/interfaces/PaintingMachine.js | 30 +- .../tgui/styles/interfaces/NtosMessenger.scss | 126 +++ tgui/packages/tgui/styles/main.scss | 19 +- 45 files changed, 2219 insertions(+), 895 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_modular_computer.dm create mode 100644 code/modules/modular_computers/file_system/picture_file.dm create mode 100644 code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm create mode 100644 code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm delete mode 100644 code/modules/modular_computers/file_system/programs/ntmessenger.dm delete mode 100644 tgui/packages/tgui/interfaces/NtosMessenger.js create mode 100644 tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx create mode 100644 tgui/packages/tgui/interfaces/NtosMessenger/index.tsx create mode 100644 tgui/packages/tgui/interfaces/NtosMessenger/types.tsx create mode 100644 tgui/packages/tgui/styles/interfaces/NtosMessenger.scss diff --git a/code/__DEFINES/dcs/signals/signals_datum.dm b/code/__DEFINES/dcs/signals/signals_datum.dm index a8da7781c37..be48a21bfb3 100644 --- a/code/__DEFINES/dcs/signals/signals_datum.dm +++ b/code/__DEFINES/dcs/signals/signals_datum.dm @@ -42,9 +42,3 @@ #define COMSIG_GASMIX_REMOVED "comsig_gasmix_removed" /// From /datum/gas_mixture/proc/react: () #define COMSIG_GASMIX_REACTED "comsig_gasmix_reacted" - -// Modular computer's file signals. Tells the program datum something is going on. -/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host) -#define COMSIG_MODULAR_COMPUTER_FILE_STORE "comsig_modular_computer_file_store" -/// From /obj/item/modular_computer/proc/store_file: () -#define COMSIG_MODULAR_COMPUTER_FILE_DELETED "comsig_modular_computer_file_deleted" diff --git a/code/__DEFINES/dcs/signals/signals_modular_computer.dm b/code/__DEFINES/dcs/signals/signals_modular_computer.dm new file mode 100644 index 00000000000..950262a5fe1 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_modular_computer.dm @@ -0,0 +1,15 @@ +// Various modular computer signals. + +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_storing) +#define COMSIG_MODULAR_COMPUTER_FILE_STORE "comsig_modular_computer_file_store" +/// From /obj/item/modular_computer/proc/remove_file: (datum/computer_file/file_removing) +#define COMSIG_MODULAR_COMPUTER_FILE_DELETE "comsig_modular_computer_file_delete" +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host) +#define COMSIG_COMPUTER_FILE_STORE "comsig_computer_file_store" +/// From /obj/item/modular_computer/proc/store_file: () +#define COMSIG_COMPUTER_FILE_DELETE "comsig_computer_file_delete" + +/// from /obj/item/modular_computer/imprint_id(): (name, job) +#define COMSIG_MODULAR_PDA_IMPRINT_UPDATED "comsig_modular_pda_imprint_updated" +/// from /obj/item/modular_computer/reset_id(): () +#define COMSIG_MODULAR_PDA_IMPRINT_RESET "comsig_modular_pda_imprint_reset" diff --git a/code/__HELPERS/chat_filter.dm b/code/__HELPERS/chat_filter.dm index eff2be71c96..b395da3d7aa 100644 --- a/code/__HELPERS/chat_filter.dm +++ b/code/__HELPERS/chat_filter.dm @@ -14,14 +14,14 @@ return null -/// Given a text, will return what word is on the IC filter, ignoring words allowed on the PDA, with the reason. +/// Given a text, will return what word is on the soft IC filter, with the reason. /// Returns null if the message is OK. -/proc/is_ic_filtered_for_pdas(message) - if (config.ic_outside_pda_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.ic_outside_pda_filter_regex) +/proc/is_soft_ic_filtered(message) + if (config.soft_ic_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.soft_ic_filter_regex) return list( matched_group, - config.ic_filter_reasons[matched_group] || config.shared_filter_reasons[matched_group], + config.soft_ic_filter_reasons[matched_group] || config.soft_ic_outside_pda_filter_reasons[matched_group] || config.soft_shared_filter_reasons[matched_group], ) return null @@ -35,14 +35,40 @@ return null -/// Given a text, will return what word is on the soft IC filter, with the reason. +/// Given a text, will return that word is on the soft OOC filter, with the reason. /// Returns null if the message is OK. -/proc/is_soft_ic_filtered(message) - if (config.soft_ic_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.soft_ic_filter_regex) +/proc/is_soft_ooc_filtered(message) + if (config.soft_ooc_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.soft_ooc_filter_regex) + return list(matched_group, config.soft_shared_filter_reasons[matched_group]) + + return null + +/// Checks a PDA message against the IC/Soft IC filter. Returns TRUE if the message should be sent. +/// Notifies the user passed in arguments if the message matched either filter. +/proc/check_pda_message_against_filter(message, mob/user) + var/list/filter_result = CAN_BYPASS_FILTER(user) ? null : is_ic_filtered_for_pdas(message) + if (filter_result) + REPORT_CHAT_FILTER_TO_USER(user, filter_result) + return FALSE + + var/list/soft_filter_result = CAN_BYPASS_FILTER(user) ? null : is_soft_ic_filtered_for_pdas(message) + if (soft_filter_result) + if(tgui_alert(user, "Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to send it?", "Soft Blocked Word", list("Yes", "No")) != "Yes") + return FALSE + message_admins("[ADMIN_LOOKUPFLW(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"") + log_admin_private("[key_name(user)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"") + + return TRUE + +/// Given a text, will return what word is on the IC filter, ignoring words allowed on the PDA, with the reason. +/// Returns null if the message is OK. +/proc/is_ic_filtered_for_pdas(message) + if (config.ic_outside_pda_filter_regex?.Find(message)) + var/matched_group = GET_MATCHED_GROUP(config.ic_outside_pda_filter_regex) return list( matched_group, - config.soft_ic_filter_reasons[matched_group] || config.soft_ic_outside_pda_filter_reasons[matched_group] || config.soft_shared_filter_reasons[matched_group], + config.ic_filter_reasons[matched_group] || config.shared_filter_reasons[matched_group], ) return null @@ -59,15 +85,6 @@ return null -/// Given a text, will return that word is on the soft OOC filter, with the reason. -/// Returns null if the message is OK. -/proc/is_soft_ooc_filtered(message) - if (config.soft_ooc_filter_regex?.Find(message)) - var/matched_group = GET_MATCHED_GROUP(config.soft_ooc_filter_regex) - return list(matched_group, config.soft_shared_filter_reasons[matched_group]) - - return null - /// Logs to the filter log with the given message, match, and scope /proc/log_filter(scope, message, filter_result) log_filter_raw("[scope] filter:\n\tMessage: [message]\n\tFilter match: [filter_result[CHAT_FILTER_INDEX_WORD]]") diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index b21cbe22ccb..e747da43c4b 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -141,11 +141,11 @@ /proc/cmp_typepaths_asc(A, B) return sorttext("[B]","[A]") -/proc/cmp_pdaname_asc(obj/item/modular_computer/A, obj/item/modular_computer/B) - return sorttext(B?.saved_identification, A?.saved_identification) +/proc/cmp_pdaname_asc(datum/computer_file/program/messenger/A, datum/computer_file/program/messenger/B) + return sorttext(B?.computer?.saved_identification, A?.computer?.saved_identification) -/proc/cmp_pdajob_asc(obj/item/modular_computer/A, obj/item/modular_computer/B) - return sorttext(B?.saved_job, A?.saved_job) +/proc/cmp_pdajob_asc(datum/computer_file/program/messenger/A, datum/computer_file/program/messenger/B) + return sorttext(B?.computer?.saved_job, A?.computer?.saved_job) /proc/cmp_num_string_asc(A, B) return text2num(A) - text2num(B) diff --git a/code/controllers/subsystem/modular_computers.dm b/code/controllers/subsystem/modular_computers.dm index 0c4d88c55e3..ad0be83bb52 100644 --- a/code/controllers/subsystem/modular_computers.dm +++ b/code/controllers/subsystem/modular_computers.dm @@ -19,6 +19,7 @@ SUBSYSTEM_DEF(modular_computers) var/intrusion_detection_enabled = TRUE ///Boolean to show a message warning if there's an active intrusion for Wirecarp users. var/intrusion_detection_alarm = FALSE + var/next_picture_id = 0 /datum/controller/subsystem/modular_computers/Initialize() build_software_lists() @@ -77,4 +78,13 @@ SUBSYSTEM_DEF(modular_computers) logs = list() add_log("-!- LOGS DELETED BY SYSTEM OPERATOR -!-") +/** + * Returns a name which a /datum/picture can be assigned to. + * Use this function to get asset names and to avoid cache duplicates/overwriting. + */ +/datum/controller/subsystem/modular_computers/proc/get_next_picture_name() + var/next_uid = next_picture_id + next_picture_id++ + return "ntos_picture_[next_uid].png" + #undef MAX_LOG_COUNT diff --git a/code/datums/records/crime.dm b/code/datums/records/crime.dm index 77e3d5b1001..a22ce7c816a 100644 --- a/code/datums/records/crime.dm +++ b/code/datums/records/crime.dm @@ -47,16 +47,17 @@ /// Sends a citation alert message to the target's PDA. /datum/crime/citation/proc/alert_owner(mob/sender, atom/source, target_name, message) - for(var/obj/item/modular_computer/tablet in GLOB.TabletMessengers) - if(tablet.saved_identification != target_name) + for(var/messenger_ref in GLOB.pda_messengers) + var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref] + if(messenger.computer.saved_identification != target_name) continue - var/datum/signal/subspace/messaging/tablet_msg/signal = new(source, list( - name = "Security Citation", - job = "Citation Server", - message = message, - targets = list(tablet), - automated = TRUE + var/datum/signal/subspace/messaging/tablet_message/signal = new(source, list( + "fakename" = "Security Citation", + "fakejob" = "Citation Server", + "message" = message, + "targets" = list(messenger), + "automated" = TRUE )) signal.send_to_receivers() sender.log_message("(PDA: Citation Server) sent \"[message]\" to [signal.format_target()]", LOG_PDA) diff --git a/code/game/machinery/PDApainter.dm b/code/game/machinery/PDApainter.dm index ad6104fda75..9c55051ca60 100644 --- a/code/game/machinery/PDApainter.dm +++ b/code/game/machinery/PDApainter.dm @@ -335,6 +335,12 @@ stored_pda.icon_state = initial(pda_path.icon_state) stored_pda.desc = initial(pda_path.desc) + return TRUE + if("reset_pda") + if((machine_stat & BROKEN) || !stored_pda) + return TRUE + + stored_pda.reset_imprint() return TRUE if("trim_card") if((machine_stat & BROKEN) || !stored_id_card) diff --git a/code/game/machinery/telecomms/computers/message.dm b/code/game/machinery/telecomms/computers/message.dm index 4db37cea2d8..e2a669804a1 100644 --- a/code/game/machinery/telecomms/computers/message.dm +++ b/code/game/machinery/telecomms/computers/message.dm @@ -210,14 +210,16 @@ var/job = tgui_input_text(usr, "What is the sender's job?", "Job") var/recipient + var/list/tablet_to_messenger = list() var/list/viewable_tablets = list() - for (var/obj/item/modular_computer/tablet as anything in GLOB.TabletMessengers) - var/datum/computer_file/program/messenger/message_app = locate() in tablet.stored_files + for (var/messenger_ref in GLOB.pda_messengers) + var/datum/computer_file/program/messenger/message_app = GLOB.pda_messengers[messenger_ref] if(!message_app || message_app.invisible) continue - if(!tablet.saved_identification) + if(!message_app.computer.saved_identification) continue - viewable_tablets += tablet + viewable_tablets += message_app.computer + tablet_to_messenger[message_app.computer] = message_app if(length(viewable_tablets) > 0) recipient = tgui_input_list(usr, "Select a tablet from the list", "Tablet Selection", viewable_tablets) else @@ -234,14 +236,11 @@ notice_message = "NOTICE: No message entered!" return attack_hand(usr) - var/datum/signal/subspace/messaging/tablet_msg/signal = new(src, list( - "name" = "[sender]", - "job" = "[job]", - "message" = html_decode(message), - "ref" = REF(src), - "targets" = list(recipient), - "rigged" = FALSE, - "automated" = FALSE, + var/datum/signal/subspace/messaging/tablet_message/signal = new(src, list( + "fakename" = "[sender]", + "fakejob" = "[job]", + "message" = message, + "targets" = list(tablet_to_messenger[recipient]), )) // This will log the signal and transmit it to the target linkedServer.receive_information(signal, null) diff --git a/code/game/machinery/telecomms/machines/broadcaster.dm b/code/game/machinery/telecomms/machines/broadcaster.dm index 8f0ccea7a86..56b53437e63 100644 --- a/code/game/machinery/telecomms/machines/broadcaster.dm +++ b/code/game/machinery/telecomms/machines/broadcaster.dm @@ -25,6 +25,9 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages return if(!signal.data["message"]) return + var/signal_message = "[signal.frequency]:[signal.data["message"]]:[signal.data["name"]]" + if(signal_message in GLOB.recentmessages) + return // Prevents massive radio spam signal.mark_done() @@ -36,9 +39,6 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages if (T) signal.levels |= SSmapping.get_connected_levels(T) - var/signal_message = "[signal.frequency]:[signal.data["message"]]:[signal.data["name"]]" - if(signal_message in GLOB.recentmessages) - return GLOB.recentmessages.Add(signal_message) if(signal.data["slow"] > 0) diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm index b721e7681f5..1e012d10027 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -131,14 +131,12 @@ return // log the signal - if(istype(signal, /datum/signal/subspace/messaging/tablet_msg)) - var/datum/signal/subspace/messaging/tablet_msg/PDAsignal = signal - var/datum/data_tablet_msg/msg = new(PDAsignal.format_target(), "[PDAsignal.data["name"]] ([PDAsignal.data["job"]])", PDAsignal.data["message"], PDAsignal.data["photo"]) - pda_msgs += msg - signal.logged = msg + if(istype(signal, /datum/signal/subspace/messaging/tablet_message)) + var/datum/signal/subspace/messaging/tablet_message/PDAsignal = signal + var/datum/data_tablet_msg/log_message = new(PDAsignal.format_target(), PDAsignal.format_sender(), PDAsignal.format_message(), PDAsignal.format_photo_path()) + pda_msgs += log_message else if(istype(signal, /datum/signal/subspace/messaging/rc)) var/datum/data_rc_msg/msg = new(signal.data["rec_dpt"], signal.data["send_dpt"], signal.data["message"], signal.data["stamped"], signal.data["verified"], signal.data["priority"]) - signal.logged = msg if(signal.data["send_dpt"]) // don't log messages not from a department but allow them to work rc_msgs += msg signal.data["reject"] = FALSE @@ -158,7 +156,6 @@ /datum/signal/subspace/messaging frequency = FREQ_COMMON server_type = /obj/machinery/telecomms/message_server - var/datum/logged /datum/signal/subspace/messaging/New(init_source, init_data) source = init_source @@ -175,28 +172,31 @@ return copy // Tablet message signal datum -/datum/signal/subspace/messaging/tablet_msg/proc/format_target() - if (length(data["targets"]) > 1) +/datum/signal/subspace/messaging/tablet_message/proc/format_target() + if (data["everyone"]) return "Everyone" - var/obj/item/modular_computer/target = data["targets"][1] + var/datum/computer_file/program/messenger/target_app = data["targets"][1] + var/obj/item/modular_computer/target = target_app.computer return "[target.saved_identification] ([target.saved_job])" -/datum/signal/subspace/messaging/tablet_msg/proc/format_message() - return "\"[data["message"]]\"" +/datum/signal/subspace/messaging/tablet_message/proc/format_sender() + var/display_name = get_messenger_name(locate(data["ref"])) + return display_name ? display_name : STRINGIFY_PDA_TARGET(data["fakename"], data["fakejob"]) -/datum/signal/subspace/messaging/tablet_msg/broadcast() - if (!logged) // Can only go through if a message server logs it - return - for (var/obj/item/modular_computer/comp in data["targets"]) - if(!QDELETED(comp)) - for(var/datum/computer_file/program/messenger/app in comp.stored_files) - if(!QDELETED(app)) - app.receive_message(src) +/datum/signal/subspace/messaging/tablet_message/proc/format_message() + return data["message"] + +/datum/signal/subspace/messaging/tablet_message/proc/format_photo_path() + return data["photo"] + +/datum/signal/subspace/messaging/tablet_message/broadcast() + for (var/datum/computer_file/program/messenger/app in data["targets"]) + if(!QDELETED(app)) + app.receive_message(src) + data["targets"] = null // Request Console signal datum /datum/signal/subspace/messaging/rc/broadcast() - if (!logged) // Like /pda, only if logged - return var/recipient_department = ckey(data["recipient_department"]) for (var/obj/machinery/requests_console/console in GLOB.req_console_all) if(ckey(console.department) == recipient_department || (data["ore_update"] && console.receive_ore_updates)) @@ -207,8 +207,8 @@ var/sender = "Unspecified" var/recipient = "Unspecified" var/message = "Blank" // transferred message - var/datum/picture/picture // attached photo - var/automated = 0 //automated message + var/picture_asset_key // attached photo path + var/automated = FALSE // automated message /datum/data_tablet_msg/New(param_rec, param_sender, param_message, param_photo) if(param_rec) @@ -218,18 +218,7 @@ if(param_message) message = param_message if(param_photo) - picture = param_photo - -/datum/data_tablet_msg/Topic(href,href_list) - ..() - if(href_list["photo"]) - var/mob/M = usr - M << browse_rsc(picture.picture_image, "pda_photo.png") - M << browse("PDA Photo" \ - + "" \ - + "" \ - + "", "window=pdaphoto;size=[picture.psize_x]x[picture.psize_y];can-close=true") - onclose(M, "pdaphoto") + picture_asset_key = param_photo /datum/data_rc_msg var/rec_dpt = "Unspecified" // receiving department diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index 221a418c1c3..3c7880ea011 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -303,11 +303,8 @@ channel = null // Nearby active jammers prevent the message from transmitting - var/turf/position = get_turf(src) - for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers) - var/turf/jammer_turf = get_turf(jammer) - if(position?.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range) && !syndie) - return + if(is_within_radio_jammer_range(src) && !syndie) + return // Determine the identity information which will be attached to the signal. var/atom/movable/virtualspeaker/speaker = new(null, talking_movable, src) diff --git a/code/game/objects/items/devices/traitordevices.dm b/code/game/objects/items/devices/traitordevices.dm index d0176835931..0e905f4a8ee 100644 --- a/code/game/objects/items/devices/traitordevices.dm +++ b/code/game/objects/items/devices/traitordevices.dm @@ -274,6 +274,12 @@ effective or pretty fucking useless. animate(user,alpha = clamp(255 - charge,0,255),time = 10) +/// Checks if a given atom is in range of a radio jammer, returns TRUE if it is. +/proc/is_within_radio_jammer_range(atom/source) + for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers) + if(IN_GIVEN_RANGE(source, jammer, jammer.range)) + return TRUE + return FALSE /obj/item/jammer name = "radio jammer" diff --git a/code/modules/admin/admin_pda_message.dm b/code/modules/admin/admin_pda_message.dm index 2aeca2fd58c..44ca86f893e 100644 --- a/code/modules/admin/admin_pda_message.dm +++ b/code/modules/admin/admin_pda_message.dm @@ -13,7 +13,7 @@ if(!check_rights(R_ADMIN)) return - if(!length(GLOB.TabletMessengers)) + if(!length(GLOB.pda_messengers)) to_chat(usr, span_warning("ERROR: There are no users you can send a message to")) return @@ -22,15 +22,6 @@ /// Panel /datum/admin_pda_panel - /// PDA which have players(and we can send them message) - var/available_pdas = list() - -/datum/admin_pda_panel/New() - for(var/obj/item/modular_computer/computer in GLOB.TabletMessengers) - for(var/datum/computer_file/program/messenger/app in computer.stored_files) - if(!computer.saved_identification || !computer.saved_job || app.monitor_hidden || app.invisible) - continue - available_pdas += list(list("tablet" = computer, "name" = computer.saved_identification)) /datum/admin_pda_panel/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) @@ -43,10 +34,15 @@ /datum/admin_pda_panel/ui_static_data(mob/user) var/list/data = list() - data["users"] = list() - - for(var/username in available_pdas) - data["users"] += username["name"] + var/list/available_messengers = list() + for(var/messenger_ref in get_messengers_sorted_by_name()) + var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref] + available_messengers[REF(messenger)] = list( + ref = REF(messenger), + username = get_messenger_name(messenger), + invisible = messenger.invisible, + ) + data["users"] = available_messengers return data /datum/admin_pda_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) @@ -60,27 +56,45 @@ if("sendMessage") var/targets = list() var/spam = params["spam"] - for(var/target in available_pdas) - if(!spam && target["name"] != params["user"]) - continue - targets += target["tablet"] + var/ref = params["ref"] + var/force = params["force"] + if(!spam && (ref in GLOB.pda_messengers)) + targets += GLOB.pda_messengers[ref] + else + for(var/messenger_ref in get_messengers_sorted_by_name()) + var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref] + if(messenger.invisible && !params["include_invisible"]) + continue + targets += messenger if(!length(targets)) - to_chat(usr, span_warning("ERROR: Target is unavaiable(or not choosed).")) - return + to_chat(usr, span_warning("ERROR: Target is unavailable.")) + return FALSE - var/datum/signal/subspace/messaging/tablet_msg/signal = new(targets[1], list( - "name" = params["name"], - "job" = params["job"], - "message" = html_decode(params["message"]), - "ref" = FALSE, + var/datum/signal/subspace/messaging/tablet_message/signal = new(null, list( + "fakename" = params["name"], + "fakejob" = params["job"], + "message" = params["message"], + "everyone" = spam, + "ref" = null, "targets" = targets, - "emojis" = FALSE, "rigged" = FALSE, - "photo" = FALSE, "automated" = FALSE, )) - signal.send_to_receivers() - message_admins("[key_name_admin(usr)] has send custom PDA message to [spam ? "everyone" : params["user"]].") - log_admin("[key_name(usr)] has send custom PDA message to [spam ? "everyone" : params["user"]]. Message: [params["message"]].") + if(force) + signal.broadcast() + else + signal.levels = SSmapping.levels_by_trait(ZTRAIT_STATION) + signal.send_to_receivers() + + if(!(force || signal.data["reject"])) + to_chat(usr, span_warning("ERROR: PDA message was rejected by the telecomms setup.")) + return FALSE + + var/recipient = spam ? "everyone" : get_messenger_name(targets[1]) + + message_admins("[key_name_admin(usr)] sent a custom PDA message to [recipient].") + log_admin("[key_name(usr)] sent a custom PDA message to [recipient]. Message: [params["message"]].") + log_pda("[key_name(usr)] sent an admin custom PDA message to [recipient]. Message: [params["message"]]") + return TRUE diff --git a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm index db8185c43ed..f619a429269 100644 --- a/code/modules/clothing/chameleon/chameleon_action_subtypes.dm +++ b/code/modules/clothing/chameleon/chameleon_action_subtypes.dm @@ -29,7 +29,7 @@ ..() var/obj/item/modular_computer/pda/agent_pda = target if(istype(agent_pda) && istype(job_datum)) - agent_pda.saved_job = job_datum.title + agent_pda.imprint_id(job_name = job_datum.title) /datum/action/item_action/chameleon/change/headset chameleon_type = /obj/item/radio/headset diff --git a/code/modules/clothing/outfits/ert.dm b/code/modules/clothing/outfits/ert.dm index 4217c979398..e365c685546 100644 --- a/code/modules/clothing/outfits/ert.dm +++ b/code/modules/clothing/outfits/ert.dm @@ -168,8 +168,7 @@ return var/obj/item/modular_computer/pda/heads/pda = H.r_store - pda.saved_identification = H.real_name - pda.saved_job = "CentCom Official" + pda.imprint_id(H.real_name, "CentCom Official") var/obj/item/card/id/W = H.wear_id W.registered_name = H.real_name diff --git a/code/modules/clothing/outfits/standard.dm b/code/modules/clothing/outfits/standard.dm index a130bc6d09d..15ede663374 100644 --- a/code/modules/clothing/outfits/standard.dm +++ b/code/modules/clothing/outfits/standard.dm @@ -217,8 +217,7 @@ sec_briefcase.contents += new /obj/item/grenade/c4/x4 var/obj/item/modular_computer/pda/heads/pda = H.belt - pda.saved_identification = H.real_name - pda.saved_job = "Reaper" + pda.imprint_id(H.real_name, "Reaper") var/obj/item/card/id/W = H.wear_id W.registered_name = H.real_name diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index 2031a90a28c..486eb7caff4 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -347,8 +347,7 @@ var/obj/item/modular_computer/pda/pda = equipped.get_item_by_slot(pda_slot) if(istype(pda)) - pda.saved_identification = equipped.real_name - pda.saved_job = equipped_job.title + pda.imprint_id(equipped.real_name, equipped_job.title) pda.update_ringtone(equipped_job.job_tone) pda.UpdateDisplay() diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 60def9a09b6..72f96725387 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -161,16 +161,18 @@ GLOBAL_LIST_EMPTY(security_officer_distribution) partners += partner.real_name if (partners.len) - for (var/obj/item/modular_computer/pda as anything in GLOB.TabletMessengers) - if (pda.saved_identification in partners) - targets += pda + for(var/messenger_ref in GLOB.pda_messengers) + var/datum/computer_file/program/messenger/messenger = GLOB.pda_messengers[messenger_ref] + if(!(messenger.computer?.saved_identification in partners)) + continue + targets += messenger if (!targets.len) return - var/datum/signal/subspace/messaging/tablet_msg/signal = new(announcement_system, list( - "name" = "Security Department Update", - "job" = "Automated Announcement System", + var/datum/signal/subspace/messaging/tablet_message/signal = new(announcement_system, list( + "fakename" = "Security Department Update", + "fakejob" = "Automated Announcement System", "message" = "Officer [officer.real_name] has been assigned to your department, [department].", "targets" = targets, "automated" = TRUE, diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index a2f3fc35778..12a2b51f484 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -923,7 +923,7 @@ if(oldname != real_name) if(eyeobj) eyeobj.name = "[newname] (AI Eye)" - modularInterface.saved_identification = real_name + modularInterface.imprint_id(name = real_name) // Notify Cyborgs for(var/mob/living/silicon/robot/Slave in connected_robots) diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index d773e49a003..6222d31881e 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -698,7 +698,7 @@ notify_ai(AI_NOTIFICATION_CYBORG_RENAMED, oldname, newname) if(!QDELETED(builtInCamera)) builtInCamera.c_tag = real_name - modularInterface.saved_identification = real_name + modularInterface.imprint_id(name = real_name) custom_name = newname diff --git a/code/modules/mob/living/silicon/robot/robot_defines.dm b/code/modules/mob/living/silicon/robot/robot_defines.dm index a39bfb4b623..d414a55000e 100644 --- a/code/modules/mob/living/silicon/robot/robot_defines.dm +++ b/code/modules/mob/living/silicon/robot/robot_defines.dm @@ -213,8 +213,7 @@ /mob/living/silicon/robot/model/syndicate/create_modularInterface() if(!modularInterface) modularInterface = new /obj/item/modular_computer/pda/silicon/cyborg/syndicate(src) - modularInterface.saved_identification = real_name - modularInterface.saved_job = "Cyborg" + modularInterface.imprint_id(job_name = "Cyborg") return ..() /mob/living/silicon/robot/model/syndicate/proc/show_playstyle() diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 16ebd6b3eaf..1b20b5d4b15 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -91,19 +91,20 @@ /mob/living/silicon/proc/create_modularInterface() if(!modularInterface) modularInterface = new /obj/item/modular_computer/pda/silicon(src) + var/job_name = "" if(isAI(src)) - modularInterface.saved_job = "AI" + job_name = "AI" if(ispAI(src)) - modularInterface.saved_job = "pAI Messenger" + job_name = "pAI Messenger" modularInterface.layer = ABOVE_HUD_PLANE SET_PLANE_EXPLICIT(modularInterface, ABOVE_HUD_PLANE, src) - modularInterface.saved_identification = real_name || name + modularInterface.imprint_id(real_name || name, job_name) /mob/living/silicon/robot/create_modularInterface() if(!modularInterface) modularInterface = new /obj/item/modular_computer/pda/silicon/cyborg(src) - modularInterface.saved_job = "Cyborg" + modularInterface.imprint_id(job_name = "Cyborg") return ..() /mob/living/silicon/med_hud_set_health() @@ -467,4 +468,4 @@ if(!modularInterface) stack_trace("Silicon [src] ( [type] ) was somehow missing their integrated tablet. Please make a bug report.") create_modularInterface() - modularInterface.saved_identification = newname + modularInterface.imprint_id(name = newname) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index f3f1b9b160f..51e6de5d49b 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1218,7 +1218,7 @@ else if( search_pda && istype(A, /obj/item/modular_computer/pda) ) var/obj/item/modular_computer/pda/PDA = A if(PDA.saved_identification == oldname) - PDA.saved_identification = newname + PDA.imprint_id(name = newname) PDA.UpdateDisplay() if(!search_id) break diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index a8e07b55f1a..18a17311c3f 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -1,5 +1,3 @@ -GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar to GLOB.PDAs (used primarily with ntmessenger.dm) - // This is the base type of computer // Other types expand it - tablets and laptops are subtypes // consoles use "procssor" item that is held inside it. @@ -134,10 +132,9 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar if(internal_cell) internal_cell = new internal_cell(src) - update_appearance() - register_context() - Add_Messenger() install_default_programs() + register_context() + update_appearance() /obj/item/modular_computer/proc/install_default_programs() SHOULD_CALL_PARENT(FALSE) @@ -151,7 +148,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar //Some components will actually try and interact with this, so let's do it later QDEL_NULL(soundloop) QDEL_LIST(stored_files) - Remove_Messenger() if(istype(inserted_disk)) QDEL_NULL(inserted_disk) @@ -647,6 +643,21 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar enabled = FALSE update_appearance() +///Imprints name and job into the modular computer, and calls back to necessary functions. +///Acts as a replacement to directly setting the imprints fields. All fields are optional, the proc will try to fill in missing gaps. +/obj/item/modular_computer/proc/imprint_id(name = null, job_name = null) + saved_identification = name || computer_id_slot?.registered_name || saved_identification + saved_job = job_name || computer_id_slot?.assignment || saved_job + SEND_SIGNAL(src, COMSIG_MODULAR_PDA_IMPRINT_UPDATED, saved_identification, saved_job) + UpdateDisplay() + +///Resets the imprinted name and job back to null. +/obj/item/modular_computer/proc/reset_imprint() + saved_identification = null + saved_job = null + SEND_SIGNAL(src, COMSIG_MODULAR_PDA_IMPRINT_RESET) + UpdateDisplay() + /obj/item/modular_computer/ui_action_click(mob/user, actiontype) if(istype(actiontype, /datum/action/item_action/toggle_computer_light)) toggle_flashlight() @@ -722,6 +733,14 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar to_chat(user, span_notice("You plug \the [attacking_item] to \the [src].")) return + if(istype(attacking_item, /obj/item/photo)) + var/obj/item/photo/attacking_photo = attacking_item + if(store_file(new /datum/computer_file/picture(attacking_photo.picture))) + balloon_alert(user, "photo scanned") + else + balloon_alert(user, "no space!") + return + // Check if any Applications need it for(var/datum/computer_file/item_holding_app as anything in stored_files) if(item_holding_app.application_attackby(attacking_item, user)) @@ -818,12 +837,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar return physical.Adjacent(neighbor) return ..() -/obj/item/modular_computer/proc/Add_Messenger() - GLOB.TabletMessengers += src - -/obj/item/modular_computer/proc/Remove_Messenger() - GLOB.TabletMessengers -= src - ///Returns a string of what to send at the end of messenger's messages. /obj/item/modular_computer/proc/get_messenger_ending() return "Sent from my PDA" diff --git a/code/modules/modular_computers/computers/item/computer_files.dm b/code/modules/modular_computers/computers/item/computer_files.dm index 296f5be0992..6b6fbd179f2 100644 --- a/code/modules/modular_computers/computers/item/computer_files.dm +++ b/code/modules/modular_computers/computers/item/computer_files.dm @@ -16,7 +16,8 @@ file_storing.computer = src used_capacity += file_storing.size - SEND_SIGNAL(file_storing, COMSIG_MODULAR_COMPUTER_FILE_STORE, src) + SEND_SIGNAL(file_storing, COMSIG_COMPUTER_FILE_STORE, src) + SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, file_storing) return TRUE /** @@ -37,7 +38,8 @@ stored_files.Remove(file_removing) used_capacity -= file_removing.size - SEND_SIGNAL(file_removing, COMSIG_MODULAR_COMPUTER_FILE_DELETED) + SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_DELETE, file_removing) + SEND_SIGNAL(file_removing, COMSIG_COMPUTER_FILE_DELETE) qdel(file_removing) return TRUE @@ -73,14 +75,34 @@ * If a computer disk is passed instead, it will check the disk over the computer. */ /obj/item/modular_computer/proc/find_file_by_name(filename, obj/item/computer_disk/target_disk) - if(!filename) + if(!istext(filename)) return null - if(target_disk) - for(var/datum/computer_file/file as anything in target_disk.stored_files) - if(file.filename == filename) - return file - else + if(isnull(target_disk)) for(var/datum/computer_file/file as anything in stored_files) if(file.filename == filename) return file + else + for(var/datum/computer_file/file as anything in target_disk.stored_files) + if(file.filename == filename) + return file + return null + +/** + * find_file_by_uid + * + * Will check all files in this computer and returns the file with the matching uid. + * A file's uid is always unique to them, so this proc is sometimes preferable over find_file_by_name. + * If a computer disk is passed instead, it will check the disk over the computer. + */ +/obj/item/modular_computer/proc/find_file_by_uid(uid, obj/item/computer_disk/target_disk) + if(!isnum(uid)) + return null + if(isnull(target_disk)) + for(var/datum/computer_file/file as anything in stored_files) + if(file.uid == uid) + return file + else + for(var/datum/computer_file/file as anything in target_disk.stored_files) + if(file.uid == uid) + return file return null diff --git a/code/modules/modular_computers/computers/item/computer_ui.dm b/code/modules/modular_computers/computers/item/computer_ui.dm index 2a76a20ef1b..fa00efae0a2 100644 --- a/code/modules/modular_computers/computers/item/computer_ui.dm +++ b/code/modules/modular_computers/computers/item/computer_ui.dm @@ -15,6 +15,7 @@ if(!active_ui) if(active_program) active_ui = new(user, src, active_program.tgui_id, active_program.filedesc) + active_program.ui_interact(user, active_ui) else active_ui = new(user, src, "NtosMain") return active_ui.open() @@ -22,6 +23,7 @@ if(active_program) active_ui.interface = active_program.tgui_id active_ui.title = active_program.filedesc + active_program.ui_interact(user, active_ui) else active_ui.interface = "NtosMain" @@ -197,8 +199,7 @@ return TRUE if("PC_Imprint_ID") - saved_identification = computer_id_slot.registered_name - saved_job = computer_id_slot.assignment + imprint_id() UpdateDisplay() playsound(src, 'sound/machines/terminal_processing.ogg', 15, TRUE) diff --git a/code/modules/modular_computers/computers/item/disks/virus_disk.dm b/code/modules/modular_computers/computers/item/disks/virus_disk.dm index d566c1f6f44..e3eac7736f5 100644 --- a/code/modules/modular_computers/computers/item/disks/virus_disk.dm +++ b/code/modules/modular_computers/computers/item/disks/virus_disk.dm @@ -9,7 +9,7 @@ ///How many charges the virus has left var/charges = 5 -/obj/item/computer_disk/virus/proc/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user) +/obj/item/computer_disk/virus/proc/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message) if(charges <= 0) to_chat(user, span_notice("ERROR: Out of charges.")) return FALSE @@ -26,7 +26,7 @@ /obj/item/computer_disk/virus/clown name = "\improper H.O.N.K. disk" -/obj/item/computer_disk/virus/clown/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user) +/obj/item/computer_disk/virus/clown/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message) . = ..() if(!.) return FALSE @@ -43,7 +43,7 @@ /obj/item/computer_disk/virus/mime name = "\improper sound of silence disk" -/obj/item/computer_disk/virus/mime/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user) +/obj/item/computer_disk/virus/mime/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message) . = ..() if(!.) return FALSE @@ -53,7 +53,7 @@ return FALSE user.show_message(span_notice("Success!")) charges-- - app.ringer_status = FALSE + app.alert_silenced = TRUE app.ringtone = "" /** @@ -64,7 +64,7 @@ name = "\improper D.E.T.O.M.A.T.I.X. disk" charges = 6 -/obj/item/computer_disk/virus/detomatix/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user) +/obj/item/computer_disk/virus/detomatix/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message) . = ..() if(!.) return FALSE @@ -82,15 +82,18 @@ var/fakejob = sanitize_name(tgui_input_text(user, "Enter a job for the rigged message.", "Forge Message", max_length = MAX_NAME_LEN), allow_numbers = TRUE) if(!fakejob || source != original_host || !user.can_perform_action(source)) return + var/attach_fake_photo = tgui_alert(user, "Attach a fake photo?", "Forge Message", list("Yes", "No")) == "Yes" var/datum/computer_file/program/messenger/app = locate() in source.stored_files - if(!app || charges <= 0 || !app.send_message(user, list(target), rigged = REF(user), fake_name = fakename, fake_job = fakejob)) + var/datum/computer_file/program/messenger/target_app = locate() in target.stored_files + if(!app || charges <= 0 || !app.send_rigged_message(user, message, list(target_app), fakename, fakejob, attach_fake_photo)) return FALSE charges-- user.show_message(span_notice("Success!")) var/reference = REF(src) target.add_traits(list(TRAIT_PDA_CAN_EXPLODE, TRAIT_PDA_MESSAGE_MENU_RIGGED), reference) addtimer(TRAIT_CALLBACK_REMOVE(target, TRAIT_PDA_MESSAGE_MENU_RIGGED, reference), 10 SECONDS) + addtimer(TRAIT_CALLBACK_REMOVE(target, TRAIT_PDA_CAN_EXPLODE, reference), 1 MINUTES) return TRUE /** @@ -119,7 +122,7 @@ telecrystal_stack.use(telecrystal_stack.amount) -/obj/item/computer_disk/virus/frame/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user) +/obj/item/computer_disk/virus/frame/send_virus(obj/item/modular_computer/pda/source, obj/item/modular_computer/pda/target, mob/living/user, message) . = ..() if(!.) return FALSE diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm index f4d982ed35b..a74114dacd2 100644 --- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm +++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm @@ -362,7 +362,7 @@ . = ..() for(var/datum/computer_file/program/messenger/msg in stored_files) msg.mime_mode = TRUE - msg.ringer_status = FALSE + msg.alert_silenced = TRUE /obj/item/modular_computer/pda/curator name = "curator PDA" @@ -380,7 +380,7 @@ /obj/item/modular_computer/pda/curator/Initialize(mapload) . = ..() for(var/datum/computer_file/program/messenger/msg in stored_files) - msg.ringer_status = FALSE + msg.alert_silenced = TRUE /obj/item/modular_computer/pda/psychologist name = "psychologist PDA" diff --git a/code/modules/modular_computers/file_system/computer_file.dm b/code/modules/modular_computers/file_system/computer_file.dm index b26df2bf475..99fc288ffaf 100644 --- a/code/modules/modular_computers/file_system/computer_file.dm +++ b/code/modules/modular_computers/file_system/computer_file.dm @@ -19,7 +19,7 @@ /datum/computer_file/New() ..() uid = file_uid++ - RegisterSignal(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, PROC_REF(on_install)) + RegisterSignal(src, COMSIG_COMPUTER_FILE_STORE, PROC_REF(on_install)) /datum/computer_file/Destroy(force) if(computer) diff --git a/code/modules/modular_computers/file_system/picture_file.dm b/code/modules/modular_computers/file_system/picture_file.dm new file mode 100644 index 00000000000..4ba71aee3d7 --- /dev/null +++ b/code/modules/modular_computers/file_system/picture_file.dm @@ -0,0 +1,37 @@ +/** + * PNG file type + * Stores a picture which can be used by other programs. + */ +/datum/computer_file/picture + filetype = "PNG" // the superior filetype + size = 1 + /// The instance of the stored picture. + var/datum/picture/stored_picture + /// The name of the asset cache item. + /// This will be initialized after assign_path() is called. + var/picture_name + +/datum/computer_file/picture/New(datum/picture/stored_picture, picture_name) + ..() + if(isnull(stored_picture)) + return + src.filename = "[stored_picture.picture_name] ([uid])" + src.stored_picture = stored_picture + src.picture_name = picture_name + +/datum/computer_file/picture/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) + . = ..() + assign_path() + +/// Assigns an asset path to the stored image, for use in the UI. +/datum/computer_file/picture/proc/assign_path() + if(!isnull(picture_name)) + return + picture_name = SSmodular_computers.get_next_picture_name() + SSassets.transport.register_asset(picture_name, stored_picture.picture_image) + +/datum/computer_file/picture/clone(rename = FALSE) + var/datum/computer_file/picture/temp = ..() + temp.stored_picture = stored_picture + temp.picture_name = picture_name + return temp diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index 2bf9a04a176..c8034335af4 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -51,6 +51,14 @@ temp.usage_flags = usage_flags return temp +/** + * WARNING: this proc does not work the same as normal `ui_interact`, as the + * computer takes care of opening the UI. The `datum/tgui/ui` parameter will always exist. + * This proc only serves as a callback. + */ +/datum/computer_file/program/ui_interact(mob/user, datum/tgui/ui) + SHOULD_CALL_PARENT(FALSE) + ///We are not calling parent as it's handled by the computer itself, this is only called after. /datum/computer_file/program/ui_act(action, params, datum/tgui/ui, datum/ui_state/state) SHOULD_CALL_PARENT(FALSE) diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm index 778fbab578e..aa897467a8c 100644 --- a/code/modules/modular_computers/file_system/programs/frontier.dm +++ b/code/modules/modular_computers/file_system/programs/frontier.dm @@ -38,7 +38,7 @@ /datum/computer_file/program/scipaper_program/proc/recheck_file_presence() if(selected_file in computer.stored_files) return FALSE - UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED) + UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE) selected_file = null paper_to_be.set_experiment() @@ -189,12 +189,12 @@ return TRUE if("select_file") // Selecting new file will necessitate a change in paper type. This will be done on select_experiment and not here. if(selected_file) - UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED) + UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE) paper_to_be.set_experiment() // Clears the paper info. for(var/datum/computer_file/data/ordnance/ordnance_data in computer.stored_files) if(ordnance_data.uid == params["selected_uid"]) selected_file = ordnance_data - RegisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED, PROC_REF(recheck_file_presence)) + RegisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE, PROC_REF(recheck_file_presence)) return TRUE if("select_experiment") var/ex_path = text2path(params["selected_expath"]) @@ -229,7 +229,7 @@ if(linked_techweb.add_scientific_paper(paper_to_be)) computer.say("\"[paper_to_be.title]\" has been published!") paper_to_be = new - UnregisterSignal(selected_file, COMSIG_MODULAR_COMPUTER_FILE_DELETED) + UnregisterSignal(selected_file, COMSIG_COMPUTER_FILE_DELETE) selected_file = null SStgui.update_uis(src) playsound(computer, 'sound/machines/ping.ogg', 25) diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm new file mode 100644 index 00000000000..0b1514d4c04 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_data.dm @@ -0,0 +1,152 @@ +/// A list of all active and visible messengers +GLOBAL_LIST_EMPTY_TYPED(pda_messengers, /datum/computer_file/program/messenger) + +/// Registers an NTMessenger instance to the list of pda_messengers. +/proc/add_messenger(datum/computer_file/program/messenger/messenger) + var/obj/item/modular_computer/messenger_device = messenger.computer + // a bunch of empty PDAs are normally allocated, we don't want that clutter + if(!messenger_device.saved_identification || !messenger_device.saved_job) + return + + if(!istype(messenger)) + return + + var/messenger_ref = REF(messenger) + if(messenger_ref in GLOB.pda_messengers) + return + + GLOB.pda_messengers[messenger_ref] = messenger + +/// Unregisters an NTMessenger instance from the pda_messengers table. +/proc/remove_messenger(datum/computer_file/program/messenger/messenger) + if(!istype(messenger)) + return + + var/messenger_ref = REF(messenger) + if(!(messenger_ref in GLOB.pda_messengers)) + return + + GLOB.pda_messengers.Remove(messenger_ref) + +/// Gets all messengers, sorted by their name +/proc/get_messengers_sorted_by_name() + return sortTim(GLOB.pda_messengers.Copy(), GLOBAL_PROC_REF(cmp_pdaname_asc), associative = TRUE) + +/// Gets all messengers, sorted by their job +/proc/get_messengers_sorted_by_job() + return sortTim(GLOB.pda_messengers.Copy(), GLOBAL_PROC_REF(cmp_pdajob_asc), associative = TRUE) + +/// Get the display name of a messenger instance +/proc/get_messenger_name(datum/computer_file/program/messenger/messenger) + if(!istype(messenger)) + return null + var/obj/item/modular_computer/computer = messenger.computer + if(!istype(computer)) + return null + return STRINGIFY_PDA_TARGET(computer.saved_identification, computer.saved_job) + +/** + * Chat log data type, stores information about the recipient, + * the messages themselves and other metadata. + */ +/datum/pda_chat + /// The cached name of the recipient, so we can + /// identify this chat even after the recipient is deleted + var/cached_name = "Unknown" + /// The cached job of the recipient + var/cached_job = "Unknown" + /// Weakref to the recipient messenger + var/datum/weakref/recipient = null + /// A list of messages in this chat + var/list/datum/pda_message/messages = list() + /// Used to determine if we should show this in recents + var/visible_in_recents = FALSE + /// Used to determine if you can talk in a chat + var/can_reply = TRUE + /// Saved draft of a message so the sender can leave and come back later + var/message_draft = "" + /// Number of unread messages in this chat + var/unread_messages = 0 + +/datum/pda_chat/New(datum/computer_file/program/messenger/recipient) + src.recipient = WEAKREF(recipient) + src.can_reply = !isnull(recipient) + +/// Adds a message to the chat log and optionally shows the chat in recents. +/// Call this instead of adding to messages directly. +/datum/pda_chat/proc/add_message(datum/pda_message/message, show_in_recents = TRUE) + messages += message + if(!visible_in_recents && show_in_recents) + visible_in_recents = TRUE + return message + +/// Returns this datum as an associative list, used for ui_data calls. +/datum/pda_chat/proc/get_ui_data(mob/user) + var/list/data = list() + + var/list/recipient_data = list() + + recipient_data["name"] = get_recipient_name() + recipient_data["job"] = get_recipient_job() + recipient_data["ref"] = recipient?.reference + + data["ref"] = REF(src) + data["recipient"] = recipient_data + + var/list/messages_data = list() + for(var/datum/pda_message/message as anything in messages) + messages_data += list(message.get_ui_data(user)) + data["messages"] = messages_data + data["message_draft"] = message_draft + + data["visible"] = visible_in_recents + data["can_reply"] = can_reply + data["unread_messages"] = unread_messages + + return data + +/// Returns the messenger's name, caches the name in case the recipient becomes invalid later. +/datum/pda_chat/proc/get_recipient_name() + var/datum/computer_file/program/messenger/messenger = recipient?.resolve() + if(istype(messenger) && (recipient.reference in GLOB.pda_messengers)) + cached_name = messenger.computer.saved_identification + return cached_name + +/// Returns the messenger's job, caches the job in case the recipient becomes invalid later. +/datum/pda_chat/proc/get_recipient_job() + var/datum/computer_file/program/messenger/messenger = recipient?.resolve() + if(istype(messenger) && (recipient.reference in GLOB.pda_messengers)) + cached_job = messenger.computer.saved_job + return cached_job + +/** + * Chat message data type, stores data about messages themselves. + */ +/datum/pda_message + /// The message itself. + var/message + /// Whether the message is sent by the user or not. + var/outgoing + /// The name of the photo asset in the SSassets cache, the URL of which is sent to the client. + var/photo_name + /// Whether this message was sent to everyone. + var/everyone + /// The station time at which this message was made. + var/timestamp + +/datum/pda_message/New(message, outgoing, timestamp, photo_name = null, everyone = FALSE) + src.message = message + src.outgoing = outgoing + src.timestamp = timestamp + src.photo_name = photo_name + src.everyone = everyone + +/// Returns an associative list of the message's data, used for ui_data calls. +/datum/pda_message/proc/get_ui_data(mob/user) + var/list/data = list() + data["message"] = message + data["outgoing"] = outgoing + data["photo_path"] = photo_name ? SSassets.transport.get_asset_url(photo_name) : null + data["everyone"] = everyone + data["timestamp"] = timestamp + return data diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm new file mode 100644 index 00000000000..06eb507167c --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm @@ -0,0 +1,731 @@ +/// Used to generate an asset key for a temporary image unique to this user. +#define TEMP_IMAGE_PATH(ref) ("ntos_messenger[ref]_temp_image.png") +/// Purpose is evident by the name, hopefully. +#define MAX_PDA_MESSAGE_LEN 1024 +/// Format of message timestamps. +#define PDA_MESSAGE_TIMESTAMP_FORMAT "hh:mm" + +/datum/computer_file/program/messenger + filename = "nt_messenger" + filedesc = "Direct Messenger" + category = PROGRAM_CATEGORY_MISC + program_icon_state = "command" + extended_desc = "This program allows old-school communication with other modular devices." + size = 0 + undeletable = TRUE // It comes by default in tablets, can't be downloaded, takes no space and should obviously not be able to be deleted. + header_program = TRUE + available_on_ntnet = FALSE + usage_flags = PROGRAM_TABLET + ui_header = "ntnrc_idle.gif" + tgui_id = "NtosMessenger" + program_icon = "comment-alt" + alert_able = TRUE + + /// Whether the user is invisible to the message list. + var/invisible = FALSE + /// great wisdom from PDA.dm - "no spamming" (prevents people from spamming the same message over and over) + COOLDOWN_DECLARE(last_text) + /// even more wisdom from PDA.dm - "no everyone spamming" (prevents people from spamming the same message over and over) + COOLDOWN_DECLARE(last_text_everyone) + /// Whether or not we're in a mime PDA. + var/mime_mode = FALSE + /// Whether this app can send messages to all. + var/spam_mode = FALSE + + /// An asssociative list of chats we have started, format: chatref -> pda_chat. + var/list/saved_chats = list() + /// Whose chatlogs we currently have open. If we are in the contacts list, this is null. + var/viewing_messages_of = null + + /// The current ringtone (displayed in the chat when a message is received). + var/ringtone = MESSENGER_RINGTONE_DEFAULT + /// Whether or not we're sorting by job. + var/sort_by_job = TRUE + /// Whether or not we're sending and receiving messages. + var/sending_and_receiving = TRUE + /// Selected photo for sending purposes. + var/selected_image = null + /// Whether or not we're sending (or trying to send) a virus. + var/sending_virus = FALSE + +/datum/computer_file/program/messenger/on_install() + . = ..() + RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_STORE, PROC_REF(check_new_photo)) + RegisterSignal(computer, COMSIG_MODULAR_COMPUTER_FILE_DELETE, PROC_REF(check_photo_removed)) + RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_UPDATED, PROC_REF(on_imprint_added)) + RegisterSignal(computer, COMSIG_MODULAR_PDA_IMPRINT_RESET, PROC_REF(on_imprint_reset)) + +/datum/computer_file/program/messenger/proc/check_new_photo(sender, datum/computer_file/picture/storing_picture) + SIGNAL_HANDLER + if(!istype(storing_picture)) + return + update_pictures_for_all() + +/datum/computer_file/program/messenger/proc/check_photo_removed(sender, datum/computer_file/picture/photo_removed) + SIGNAL_HANDLER + if(istype(photo_removed) && selected_image == photo_removed.picture_name) + selected_image = null + +/datum/computer_file/program/messenger/proc/on_imprint_added(sender) + SIGNAL_HANDLER + add_messenger(src) + +/datum/computer_file/program/messenger/proc/on_imprint_reset(sender) + SIGNAL_HANDLER + remove_messenger(src) + saved_chats = list() + selected_image = null + viewing_messages_of = null + +/datum/computer_file/program/messenger/Destroy(force) + if(!QDELETED(computer)) + stack_trace("Attempted to qdel messenger of [computer] without qdeling computer, this will cause problems later") + remove_messenger(src) + return ..() + +/// Gets the list of available messengers +/datum/computer_file/program/messenger/proc/get_messengers() + var/list/dictionary = list() + + var/list/messengers_sorted = sort_by_job ? get_messengers_sorted_by_job() : get_messengers_sorted_by_name() + + for(var/messenger_ref in messengers_sorted) + var/datum/computer_file/program/messenger/messenger = messengers_sorted[messenger_ref] + if(messenger == src || messenger.invisible) + continue + + var/list/data = list() + data["name"] = messenger.computer.saved_identification + data["job"] = messenger.computer.saved_job + data["ref"] = REF(messenger) + + dictionary[data["ref"]] = data + + return dictionary + +/// Checks if the person can send an everyone message +/datum/computer_file/program/messenger/proc/can_send_everyone_message() + return COOLDOWN_FINISHED(src, last_text) && COOLDOWN_FINISHED(src, last_text_everyone) + +/// Gets all currently relevant photo asset keys +/datum/computer_file/program/messenger/proc/get_picture_assets() + var/list/data = list() + + for(var/datum/computer_file/picture/photo in computer.stored_files) + data |= photo.picture_name + + if(viewing_messages_of in saved_chats) + var/datum/pda_chat/chat = saved_chats[viewing_messages_of] + for(var/datum/pda_message/message as anything in chat.messages) + if(isnull(message.photo_name)) + continue + data |= message.photo_name + + if(!isnull(selected_image)) + data |= selected_image + + return data + +/// Sends new datum/picture assets to everyone +/datum/computer_file/program/messenger/proc/update_pictures_for_all() + var/list/data = get_picture_assets() + + if(isnull(computer.open_uis)) + return + + for(var/datum/tgui/window as anything in computer.open_uis) + SSassets.transport.send_assets(window.user, data) + +/datum/computer_file/program/messenger/ui_interact(mob/user, datum/tgui/ui) + var/list/data = get_picture_assets() + SSassets.transport.send_assets(user, data) + +/datum/computer_file/program/messenger/ui_state(mob/user) + if(issilicon(user)) + return GLOB.reverse_contained_state + return GLOB.default_state + +/datum/computer_file/program/messenger/ui_act(action, list/params, datum/tgui/ui) + switch(action) + if("PDA_ringSet") + var/new_ringtone = tgui_input_text(usr, "Enter a new ringtone", "Ringtone", ringtone, MESSENGER_RINGTONE_MAX_LENGTH) + var/mob/living/usr_mob = usr + if(!new_ringtone || !in_range(computer, usr_mob) || computer.loc != usr_mob) + return FALSE + + if(SEND_SIGNAL(computer, COMSIG_TABLET_CHANGE_ID, usr_mob, new_ringtone) & COMPONENT_STOP_RINGTONE_CHANGE) + return FALSE + + ringtone = new_ringtone + return TRUE + + if("PDA_toggleAlerts") + alert_silenced = !alert_silenced + return TRUE + + if("PDA_toggleSendingAndReceiving") + sending_and_receiving = !sending_and_receiving + return TRUE + + if("PDA_viewMessages") + if(viewing_messages_of in saved_chats) + var/datum/pda_chat/chat = saved_chats[viewing_messages_of] + chat.unread_messages = 0 + + viewing_messages_of = params["ref"] + + if (viewing_messages_of in saved_chats) + var/datum/pda_chat/chat = saved_chats[viewing_messages_of] + chat.visible_in_recents = TRUE + + selected_image = null + return TRUE + + if("PDA_closeMessages") + var/target = params["ref"] + + if(!(target in saved_chats)) + return FALSE + + var/datum/pda_chat/chat = saved_chats[target] + chat.visible_in_recents = FALSE + if(viewing_messages_of == target) + viewing_messages_of = null + return TRUE + + if("PDA_clearMessages") + var/chat_ref = params["ref"] + + if(chat_ref in saved_chats) + saved_chats.Remove(chat_ref) + else if(isnull(chat_ref)) + saved_chats = list() + + viewing_messages_of = null + return TRUE + + if("PDA_changeSortStyle") + sort_by_job = !sort_by_job + return TRUE + + if("PDA_sendEveryone") + if(!sending_and_receiving) + to_chat(usr, span_notice("ERROR: This device has sending disabled.")) + return FALSE + + if(!spam_mode) + to_chat(usr, span_notice("ERROR: This device does not have mass-messaging perms.")) + return FALSE + + if(!can_send_everyone_message()) + return FALSE + + return send_message_to_all(usr, params["message"]) + + if("PDA_saveMessageDraft") + var/target_chat_ref = params["ref"] + var/message_draft = params["message"] + + if(!(target_chat_ref in saved_chats)) + return FALSE + + var/datum/pda_chat/chat = saved_chats[target_chat_ref] + + chat.message_draft = message_draft + + return TRUE + + if("PDA_clearUnreads") + var/target_chat_ref = params["ref"] + + if(!(target_chat_ref in saved_chats)) + return FALSE + + var/datum/pda_chat/chat = saved_chats[target_chat_ref] + chat.unread_messages = 0 + + return TRUE + + if("PDA_sendMessage") + if(!sending_and_receiving) + to_chat(usr, span_notice("ERROR: This device has sending disabled.")) + return FALSE + + // target ref, can either be a chat in saved_chats + // or a messenger ref in GLOB.pda_messengers + var/target_ref = params["ref"] + + var/target = null + + if(target_ref in saved_chats) + target = saved_chats[target_ref] + else if(target_ref in GLOB.pda_messengers) + target = GLOB.pda_messengers[target_ref] + else + return FALSE + + if(sending_virus) + var/obj/item/computer_disk/virus/disk = computer.inserted_disk + if(!istype(disk)) + return FALSE + + var/datum/computer_file/program/messenger/target_messenger = null + + if(istype(target, /datum/pda_chat)) + var/datum/pda_chat/target_chat = target + target_messenger = target_chat.recipient?.resolve() + if(!istype(target_messenger)) + to_chat(usr, span_notice("ERROR: Recipient no longer exists.")) + return FALSE + else if(istype(target, /datum/computer_file/program/messenger)) + target_messenger = target + + return disk.send_virus(computer, target_messenger.computer, usr, params["message"]) + + return send_message(usr, params["message"], list(target)) + + if("PDA_clearPhoto") + selected_image = null + return TRUE + + if("PDA_toggleVirus") + sending_virus = !sending_virus + return TRUE + + if("PDA_selectPhoto") + if(issilicon(usr)) + return FALSE + + var/photo_uid = text2num(params["uid"]) + + var/datum/computer_file/picture/selected_photo = computer.find_file_by_uid(photo_uid) + + if(!istype(selected_photo)) + return FALSE + + selected_image = selected_photo.picture_name + return TRUE + + if("PDA_siliconSelectPhoto") + if(!issilicon(usr)) + return FALSE + var/mob/living/silicon/user = usr + if(!user.aicamera) + return FALSE + var/datum/picture/selected_photo = user.aicamera.selectpicture(user) + if(!selected_photo) + return FALSE + SSassets.transport.register_asset(TEMP_IMAGE_PATH(REF(src)), selected_photo.picture_image) + selected_image = TEMP_IMAGE_PATH(REF(src)) + update_pictures_for_all() + return TRUE + +/datum/computer_file/program/messenger/ui_static_data(mob/user) + var/list/static_data = list() + + static_data["can_spam"] = spam_mode + static_data["is_silicon"] = issilicon(user) + static_data["alert_able"] = alert_able + + return static_data + +/datum/computer_file/program/messenger/ui_data(mob/user) + var/list/data = list() + + var/list/chats_data = list() + for(var/chat_ref in saved_chats) + var/datum/pda_chat/chat = saved_chats[chat_ref] + var/list/chat_data = chat.get_ui_data(user) + chats_data[chat_ref] = chat_data + + var/list/messengers = get_messengers() + + data["owner"] = ((REF(src) in GLOB.pda_messengers) ? list( + "name" = computer.saved_identification, + "job" = computer.saved_job, + "ref" = REF(src) + ) : null) + data["saved_chats"] = chats_data + data["messengers"] = messengers + data["sort_by_job"] = sort_by_job + data["alert_silenced"] = alert_silenced + data["sending_and_receiving"] = sending_and_receiving + data["open_chat"] = viewing_messages_of + + // silicons handle selecting photos a bit differently for now + if(!issilicon(user)) + var/list/stored_photos = list() + for(var/datum/computer_file/picture/photo_file in computer.stored_files) + stored_photos += list(list( + "uid" = photo_file.uid, + "path" = SSassets.transport.get_asset_url(photo_file.picture_name) + )) + data["stored_photos"] = stored_photos + data["selected_photo_path"] = !isnull(selected_image) ? SSassets.transport.get_asset_url(selected_image) : null + data["on_spam_cooldown"] = !can_send_everyone_message() + + var/obj/item/computer_disk/virus/disk = computer.inserted_disk + if(istype(disk)) + data["virus_attach"] = TRUE + data["sending_virus"] = sending_virus + return data + +////////////////////// +// MESSAGE HANDLING // +////////////////////// + +/// Brings up the quick reply prompt to send a message. +/datum/computer_file/program/messenger/proc/quick_reply_prompt(mob/living/user, datum/pda_chat/chat) + if(!istype(chat)) + return + var/datum/computer_file/program/messenger/target = chat.recipient?.resolve() + if(!istype(target) || !istype(target.computer)) + to_chat(user, span_notice("ERROR: Recipient no longer exists.")) + chat.recipient = null + chat.can_reply = FALSE + return + var/target_name = target.computer.saved_identification + var/input_message = tgui_input_text(user, "Enter [mime_mode ? "emojis":"a message"]", "NT Messaging[target_name ? " ([target_name])" : ""]", encode = FALSE) + send_message(user, input_message, list(chat)) + +/// Helper proc that sends a message to everyone +/datum/computer_file/program/messenger/proc/send_message_to_all(mob/living/user, message) + var/list/datum/pda_chat/chats = list() + var/list/messenger_targets = list() + + for(var/mc in get_messengers()) + messenger_targets += mc + + for(var/chatref in saved_chats) + var/datum/pda_chat/chat = saved_chats[chatref] + if(!(chat.recipient?.reference in messenger_targets)) // if its in messenger_targets, it's valid + continue + messenger_targets -= chat.recipient.reference + chats += chat + + for(var/missing_messenger in messenger_targets) + var/datum/pda_chat/new_chat = create_chat(missing_messenger) + chats += new_chat + + if(send_message(user, message, chats, everyone = TRUE)) + COOLDOWN_START(src, last_text_everyone, 2 MINUTES) + +/// Creates a chat and adds it to saved_chats. Supports fake users. Returns the newly created chat. +/datum/computer_file/program/messenger/proc/create_chat(recipient_ref, name, job) + var/datum/computer_file/program/messenger/recipient = null + + if(isnull(name) && isnull(job)) + if(!(recipient_ref in GLOB.pda_messengers)) + CRASH("tried to create a chat with a messenger that isn't registered") + recipient = GLOB.pda_messengers[recipient_ref] + + var/datum/pda_chat/new_chat = new(recipient) + + // this is a chat with a "fake user" (automated or forged message) + if(!istype(recipient)) + new_chat.cached_name = name + new_chat.cached_job = job + new_chat.can_reply = FALSE + + saved_chats[REF(new_chat)] = new_chat + + return new_chat + +/// Gets the chat by the recipient, either by their name or messenger ref +/datum/computer_file/program/messenger/proc/find_chat_by_recipient(recipient, fake_user = FALSE) + for(var/chat_ref in saved_chats) + var/datum/pda_chat/chat = saved_chats[chat_ref] + if(fake_user && chat.cached_name == recipient) + return chat + else if(chat.recipient?.reference == recipient) + return chat + return null + +/// Returns a message input, sanitized and checked against the filter +/datum/computer_file/program/messenger/proc/sanitize_pda_message(message, mob/sender) + message = sanitize(trim(message, MAX_PDA_MESSAGE_LEN)) + + if(mime_mode) + message = emoji_sanitize(message) + + // check message against filter + if(!check_pda_message_against_filter(message, sender)) + return null + + return message + +/// Sends a message to targets via PDA. When sending to everyone, set `everyone` to true so the message is formatted accordingly +/datum/computer_file/program/messenger/proc/send_message(mob/living/sender, message, list/targets, everyone = FALSE) + message = sanitize_pda_message(message, sender) + + if(!message) + return FALSE + + // upgrade the image asset to a permanent key + var/photo_asset_key = selected_image + if(photo_asset_key == TEMP_IMAGE_PATH(REF(src))) + var/datum/asset_cache_item/img_asset = SSassets.cache[photo_asset_key] + photo_asset_key = SSmodular_computers.get_next_picture_name() + SSassets.transport.register_asset(photo_asset_key, img_asset.resource, img_asset.hash) + + // our sender targets + var/list/datum/computer_file/program/messenger/target_messengers = list() + var/list/datum/pda_chat/target_chats = list() + + var/should_alert = length(targets) == 1 + + // filter out invalid targets + for(var/target in targets) + var/datum/pda_chat/target_chat = null + var/datum/computer_file/program/messenger/target_messenger = null + + if(istype(target, /datum/pda_chat)) + target_chat = target + + if(!target_chat.can_reply) + if(should_alert) + to_chat(sender, span_notice("ERROR: Recipient has receiving disabled.")) + continue + + target_messenger = target_chat.recipient?.resolve() + + if(!istype(target_messenger)) + if(should_alert) + to_chat(sender, span_notice("ERROR: Recipient no longer exists.")) + target_chat.can_reply = FALSE + target_chat.recipient = null + continue + + if(!target_messenger.sending_and_receiving) + if(should_alert) + to_chat(sender, span_notice("ERROR: Recipient has receiving disabled.")) + continue + + else if(istype(target, /datum/computer_file/program/messenger)) + target_messenger = target + + if(!target_messenger.sending_and_receiving) + if(should_alert) + to_chat(sender, span_notice("ERROR: Recipient has receiving disabled.")) + continue + + target_chat = find_chat_by_recipient(REF(target)) + + if(!istype(target_chat)) + target_chat = create_chat(REF(target)) + + else + stack_trace("invalid target [target]") + continue + + target_chats += target_chat + target_messengers += target_messenger + + if(!send_message_signal(sender, message, target_messengers, photo_asset_key, everyone)) + return FALSE + + // Log it in our logs + var/datum/pda_message/message_datum = new(message, TRUE, station_time_timestamp(PDA_MESSAGE_TIMESTAMP_FORMAT), photo_asset_key, everyone) + for(var/datum/pda_chat/target_chat as anything in target_chats) + target_chat.add_message(message_datum, show_in_recents = !everyone) + target_chat.unread_messages = 0 + + // send new pictures to everyone + if(!isnull(photo_asset_key)) + update_pictures_for_all() + + // switch our chat screen after sending a message, but do it only if it's not to everyone + if(!everyone) + viewing_messages_of = REF(target_chats[1]) + + return TRUE + +/// Sends a rigged message that explodes when the recipient tries to reply or look at it. +/datum/computer_file/program/messenger/proc/send_rigged_message(mob/sender, message, list/datum/computer_file/program/messenger/targets, fake_name, fake_job, attach_fake_photo) + message = sanitize_pda_message(message, sender) + + if(!message) + return FALSE + + var/fake_photo = attach_fake_photo ? ">:3c" : null + + return send_message_signal(sender, message, targets, fake_photo, FALSE, TRUE, fake_name, fake_job) + +/datum/computer_file/program/messenger/proc/send_message_signal(mob/sender, message, list/datum/computer_file/program/messenger/targets, photo_path = null, everyone = FALSE, rigged = FALSE, fake_name = null, fake_job = null) + if(!sender.can_perform_action(computer)) + return FALSE + + if(!COOLDOWN_FINISHED(src, last_text)) + return FALSE + + if(!length(targets)) + return FALSE + + // check for jammers + if(is_within_radio_jammer_range(computer) && !rigged) + // different message so people know it's a radio jammer + to_chat(sender, span_notice("ERROR: Network unavailable, please try again later.")) + if(alert_able && !alert_silenced) + playsound(computer, 'sound/machines/terminal_error.ogg', 15, TRUE) + return FALSE + + // used for logging + var/list/stringified_targets = list() + + for(var/datum/computer_file/program/messenger/messenger as anything in targets) + stringified_targets += get_messenger_name(messenger) + + var/datum/signal/subspace/messaging/tablet_message/signal = new(computer, list( + "ref" = REF(src), + "message" = message, + "targets" = targets, + "rigged" = rigged, + "everyone" = everyone, + "photo" = photo_path, + "automated" = FALSE, + )) + if(rigged) //Will skip the message server and go straight to the hub so it can't be cheesed by disabling the message server machine + signal.data["fakename"] = fake_name + signal.data["fakejob"] = fake_job + signal.server_type = /obj/machinery/telecomms/hub + signal.data["reject"] = FALSE // Do not refuse the message + + signal.send_to_receivers() + + // If it didn't reach, note that fact + if (!signal.data["done"]) + to_chat(sender, span_notice("ERROR: Server is not responding.")) + if(alert_able && !alert_silenced) + playsound(computer, 'sound/machines/terminal_error.ogg', 15, TRUE) + return FALSE + + // Log in the talk log + sender.log_talk(message, LOG_PDA, tag="[rigged ? "Rigged" : ""] PDA: [computer.saved_identification] to [signal.format_target()]") + if(rigged) + log_bomber(sender, "sent a rigged PDA message (Name: [fake_name]. Job: [fake_job]) to [english_list(stringified_targets)] [!is_special_character(sender) ? "(SENT BY NON-ANTAG)" : ""]") + + message = emoji_parse(message) //already sent- this just shows the sent emoji as one to the sender in the to_chat + + // Show it to ghosts + var/ghost_message = span_name("[sender] [rigged ? "(as [fake_name]) Rigged " : ""]PDA Message --> [span_name("[signal.format_target()]")]: \"[signal.format_message()]\"") + for(var/mob/player_mob as anything in GLOB.current_observers_list) + if(player_mob.client && !player_mob.client?.prefs) + stack_trace("[player_mob] ([player_mob.ckey]) had null prefs, which shouldn't be possible!") + continue + + if(isobserver(player_mob) && (player_mob.client?.prefs.chat_toggles & CHAT_GHOSTPDA)) + to_chat(player_mob, "[FOLLOW_LINK(player_mob, sender)] [ghost_message]") + + to_chat(sender, span_info("PDA message sent to [signal.format_target()]: \"[message]\"")) + + if (alert_able && !alert_silenced) + computer.send_sound() + + COOLDOWN_START(src, last_text, 1 SECONDS) + + selected_image = null + return TRUE + +/datum/computer_file/program/messenger/proc/receive_message(datum/signal/subspace/messaging/tablet_message/signal) + var/datum/pda_chat/chat = null + + var/is_rigged = signal.data["rigged"] + var/is_automated = signal.data["automated"] + var/is_fake_user = is_rigged || is_automated || isnull(signal.data["ref"]) + var/fake_name = is_fake_user ? signal.data["fakename"] : null + var/fake_job = is_fake_user ? signal.data["fakejob"] : null + + var/sender_ref = signal.data["ref"] + + // don't create a new chat for rigged messages, make it a one off notif + if(!is_rigged) + var/datum/pda_message/message = new(signal.data["message"], FALSE, station_time_timestamp(PDA_MESSAGE_TIMESTAMP_FORMAT), signal.data["photo"], signal.data["everyone"]) + + chat = find_chat_by_recipient(is_fake_user ? fake_name : sender_ref, is_fake_user) + if(!istype(chat)) + chat = create_chat(!is_fake_user ? sender_ref : null, fake_name, fake_job) + chat.add_message(message) + chat.unread_messages++ + + // the recipient (us) currently has a chat with the sender open, so update their ui + if(!isnull(viewing_messages_of) && viewing_messages_of == sender_ref) + viewing_messages_of = REF(chat) + + var/mob/living/receiver_mob = null + //Check our immediate loc + if(isliving(computer.loc)) + receiver_mob = computer.loc + //Maybe they are a silicon! + else + receiver_mob = get(computer, /mob/living/silicon) + + var/should_ring = !alert_silenced || is_rigged + + if(should_ring && istype(receiver_mob) && receiver_mob.is_literate() && (receiver_mob.stat == CONSCIOUS || receiver_mob.stat == SOFT_CRIT)) + var/reply_href = signal.data["rigged"] ? "explode" : "message" + var/photo_href = signal.data["rigged"] ? "explode" : "open" + var/reply + if(is_automated) + reply = "\[Automated Message\]" + else + reply = "(Reply)" + + // resolving w/o nullcheck here, assume the messenger exists if a real person sent a message + var/datum/computer_file/program/messenger/sender_messenger = chat.recipient?.resolve() + + var/sender_title = is_fake_user ? STRINGIFY_PDA_TARGET(fake_name, fake_job) : get_messenger_name(sender_messenger) + var/sender_name = is_fake_user ? fake_name : sender_messenger.computer.saved_identification + + if (isAI(receiver_mob)) + sender_title = "[sender_title]" + + var/inbound_message = "[signal.format_message()]" + inbound_message = emoji_parse(inbound_message) + + var/photo_message = signal.data["photo"] ? " (Photo Attached)" : "" + to_chat(receiver_mob, span_infoplain("[icon2html(computer, receiver_mob)] PDA message from [sender_title], \"[inbound_message]\"[photo_message] [reply]")) + + if (alert_able && should_ring) + computer.ring(ringtone) + + SStgui.update_uis(computer) + update_pictures_for_all() + +/// topic call that answers to people pressing "(Reply)" in chat +/datum/computer_file/program/messenger/Topic(href, href_list) + ..() + + if(QDELETED(src)) + return + if(!usr.can_perform_action(computer, FORBID_TELEKINESIS_REACH)) + return + + // send an activation message and open the messenger + if(!(computer.enabled || computer.turn_on(usr, open_ui = FALSE))) + return + if(!(computer.active_program == src || computer.open_program(usr, src, open_ui = FALSE))) + return + + var/target_href = href_list["target"] + + switch(href_list["choice"]) + if("message") + if(!(target_href in saved_chats)) + return + quick_reply_prompt(usr, saved_chats[target_href]) + + if("open") + if(target_href in saved_chats) + viewing_messages_of = target_href + computer.update_tablet_open_uis(usr) + + if("explode") + if(!HAS_TRAIT(computer, TRAIT_PDA_CAN_EXPLODE)) + return + + var/obj/item/modular_computer/pda/comp = computer + comp.explode(usr, from_message_menu = TRUE) + +#undef PDA_MESSAGE_TIMESTAMP_FORMAT +#undef MAX_PDA_MESSAGE_LEN +#undef TEMP_IMAGE_PATH diff --git a/code/modules/modular_computers/file_system/programs/ntmessenger.dm b/code/modules/modular_computers/file_system/programs/ntmessenger.dm deleted file mode 100644 index e61b7643b28..00000000000 --- a/code/modules/modular_computers/file_system/programs/ntmessenger.dm +++ /dev/null @@ -1,426 +0,0 @@ -/datum/computer_file/program/messenger - filename = "nt_messenger" - filedesc = "Direct Messenger" - category = PROGRAM_CATEGORY_MISC - program_icon_state = "command" - extended_desc = "This program allows old-school communication with other modular devices." - size = 0 - undeletable = TRUE // It comes by default in tablets, can't be downloaded, takes no space and should obviously not be able to be deleted. - header_program = TRUE - available_on_ntnet = FALSE - usage_flags = PROGRAM_TABLET - ui_header = "ntnrc_idle.gif" - tgui_id = "NtosMessenger" - program_icon = "comment-alt" - alert_able = TRUE - - /// The current ringtone (displayed in the chat when a message is received). - var/ringtone = MESSENGER_RINGTONE_DEFAULT - /// Whether or not the ringtone is currently on. - var/ringer_status = TRUE - /// Whether or not we're sending and receiving messages. - var/sending_and_receiving = TRUE - /// The messages currently saved in the app. - var/messages = list() - /// great wisdom from PDA.dm - "no spamming" (prevents people from spamming the same message over and over) - var/last_text - /// even more wisdom from PDA.dm - "no everyone spamming" (prevents people from spamming the same message over and over) - var/last_text_everyone - /// Scanned photo for sending purposes. - var/datum/picture/saved_image - /// Whether the user is invisible to the message list. - var/invisible = FALSE - /// Whether or not we're currently looking at the message list. - var/viewing_messages = FALSE - // Whether or not this device is currently hidden from the message monitor. - var/monitor_hidden = FALSE - // Whether or not we're sorting by job. - var/sort_by_job = TRUE - // Whether or not we're sending (or trying to send) a virus. - var/sending_virus = FALSE - - /// The path for the current loaded image in rsc - var/photo_path - - /// Whether or not we're in a mime PDA. - var/mime_mode = FALSE - /// Whether this app can send messages to all. - var/spam_mode = FALSE - -/datum/computer_file/program/messenger/application_attackby(obj/item/attacking_item, mob/living/user) - if(!istype(attacking_item, /obj/item/photo)) - return FALSE - var/obj/item/photo/pic = attacking_item - saved_image = pic.picture - ProcessPhoto() - user.balloon_alert(user, "photo uploaded") - return TRUE - -/datum/computer_file/program/messenger/proc/ScrubMessengerList() - var/list/dictionary = list() - - for(var/obj/item/modular_computer/messenger in GetViewableDevices(sort_by_job)) - if(messenger.saved_identification && messenger.saved_job && !(messenger == computer)) - var/list/data = list() - data["name"] = messenger.saved_identification - data["job"] = messenger.saved_job - data["ref"] = REF(messenger) - - //if(data["ref"] != REF(computer)) // you cannot message yourself (despite all my rage) - dictionary += list(data) - - return dictionary - -/proc/GetViewableDevices(sort_by_job = FALSE) - var/list/dictionary = list() - - var/sortmode - if(sort_by_job) - sortmode = GLOBAL_PROC_REF(cmp_pdajob_asc) - else - sortmode = GLOBAL_PROC_REF(cmp_pdaname_asc) - - for(var/obj/item/modular_computer/P in sort_list(GLOB.TabletMessengers, sortmode)) - for(var/datum/computer_file/program/messenger/app in P.stored_files) - if(!P.saved_identification || !P.saved_job || app.invisible || app.monitor_hidden) - continue - dictionary += P - - return dictionary - -/datum/computer_file/program/messenger/proc/StringifyMessengerTarget(obj/item/modular_computer/messenger) - return "[messenger.saved_identification] ([messenger.saved_job])" - -/datum/computer_file/program/messenger/proc/ProcessPhoto() - if(saved_image) - var/icon/img = saved_image.picture_image - var/deter_path = "tmp_msg_photo[rand(0, 99999)].png" - usr << browse_rsc(img, deter_path) // funny random assignment for now, i'll make an actual key later - photo_path = deter_path - -/datum/computer_file/program/messenger/ui_state(mob/user) - if(issilicon(user)) - return GLOB.reverse_contained_state - return GLOB.default_state - -/datum/computer_file/program/messenger/ui_act(action, list/params, datum/tgui/ui) - switch(action) - if("PDA_ringSet") - var/new_ringtone = tgui_input_text(usr, "Enter a new ringtone", "Ringtone", ringtone, MESSENGER_RINGTONE_MAX_LENGTH) - var/mob/living/usr_mob = usr - if(!new_ringtone || !in_range(computer, usr_mob) || computer.loc != usr_mob) - return - - if(SEND_SIGNAL(computer, COMSIG_TABLET_CHANGE_ID, usr_mob, new_ringtone) & COMPONENT_STOP_RINGTONE_CHANGE) - return - - ringtone = new_ringtone - return TRUE - - if("PDA_ringer_status") - ringer_status = !ringer_status - return TRUE - - if("PDA_sAndR") - sending_and_receiving = !sending_and_receiving - return TRUE - - if("PDA_viewMessages") - viewing_messages = !viewing_messages - return TRUE - - if("PDA_clearMessages") - messages = list() - return TRUE - - if("PDA_changeSortStyle") - sort_by_job = !sort_by_job - return TRUE - - if("PDA_sendEveryone") - if(!sending_and_receiving) - to_chat(usr, span_notice("ERROR: Device has sending disabled.")) - return - - if(!spam_mode) - to_chat(usr, span_notice("ERROR: Device does not have mass-messaging perms.")) - return - - var/list/targets = list() - - for(var/obj/item/modular_computer/mc in GetViewableDevices()) - targets += mc - - if(targets.len > 0) - send_message(usr, targets, TRUE) - - return TRUE - - if("PDA_sendMessage") - if(!sending_and_receiving) - to_chat(usr, span_notice("ERROR: Device has sending disabled.")) - return - - var/obj/item/modular_computer/target = locate(params["ref"]) - if(!target) - return // we don't want tommy sending his messages to nullspace - - if(!(target.saved_identification == params["name"] && target.saved_job == params["job"])) - to_chat(usr, span_notice("ERROR: User no longer exists.")) - return - - for(var/datum/computer_file/program/messenger/app in computer.stored_files) - if(!app.sending_and_receiving && !sending_virus) - to_chat(usr, span_notice("ERROR: Device has receiving disabled.")) - return - - if(sending_virus) - var/obj/item/computer_disk/virus/disk = computer.inserted_disk - if(istype(disk)) - disk.send_virus(computer, target, usr) - update_static_data(usr, ui) - return TRUE - - send_message(usr, list(target)) - return TRUE - - if("PDA_clearPhoto") - saved_image = null - photo_path = null - return TRUE - - if("PDA_toggleVirus") - sending_virus = !sending_virus - return TRUE - - if("PDA_selectPhoto") - if(!issilicon(usr)) - return - var/mob/living/silicon/user = usr - if(!user.aicamera) - return - var/datum/picture/selected_photo = user.aicamera.selectpicture(user) - if(!selected_photo) - return - saved_image = selected_photo - ProcessPhoto() - return TRUE - -/datum/computer_file/program/messenger/ui_static_data(mob/user) - var/list/data = list() - data["owner"] = computer.saved_identification - return data - -/datum/computer_file/program/messenger/ui_data(mob/user) - var/list/data = list() - data["messages"] = messages - data["sortByJob"] = sort_by_job - data["isSilicon"] = issilicon(user) - data["messengers"] = ScrubMessengerList() - data["ringer_status"] = ringer_status - data["sending_and_receiving"] = sending_and_receiving - data["viewing_messages"] = viewing_messages - data["photo"] = photo_path - data["canSpam"] = spam_mode - - var/obj/item/computer_disk/virus/disk = computer.inserted_disk - if(disk && istype(disk)) - data["virus_attach"] = TRUE - data["sending_virus"] = sending_virus - return data - -////////////////////// -// MESSAGE HANDLING // -////////////////////// - -///Gets an input message from user and returns the sanitized message. -/datum/computer_file/program/messenger/proc/msg_input(mob/living/user, target_name, rigged = FALSE) - var/input_message - if(mime_mode) - input_message = emoji_sanitize(tgui_input_text(user, "Enter emojis", "NT Messaging[target_name ? " ([target_name])" : ""]")) - else - input_message = tgui_input_text(user, "Enter a message", "NT Messaging[target_name ? " ([target_name])" : ""]") - - if (!input_message || !sending_and_receiving) - return - if(!user.can_perform_action(computer)) - return - return sanitize(input_message) - -/datum/computer_file/program/messenger/proc/send_message(mob/living/user, list/obj/item/modular_computer/targets, everyone = FALSE, rigged = FALSE, fake_name = null, fake_job = null) - if(!targets.len) - return FALSE - - var/target_name = length(targets) == 1 ? targets[1].saved_identification : "Everyone" - var/message = msg_input(user, target_name, rigged) - - if(!message) - return FALSE - - if((last_text && world.time < last_text + 10) || (everyone && last_text_everyone && world.time < last_text_everyone + 2 MINUTES)) - to_chat(user, span_warning("The subspace transmitter of your tablet is still cooling down!")) - return FALSE - - var/turf/position = get_turf(computer) - for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers) - var/turf/jammer_turf = get_turf(jammer) - if(position?.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range)) - return FALSE - - var/list/filter_result = CAN_BYPASS_FILTER(user) ? null : is_ic_filtered_for_pdas(message) - if (filter_result) - REPORT_CHAT_FILTER_TO_USER(user, filter_result) - return FALSE - - var/list/soft_filter_result = CAN_BYPASS_FILTER(user) ? null : is_soft_ic_filtered_for_pdas(message) - if (soft_filter_result) - if(tgui_alert(usr,"Your message contains \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\". \"[soft_filter_result[CHAT_FILTER_INDEX_REASON]]\", Are you sure you want to send it?", "Soft Blocked Word", list("Yes", "No")) != "Yes") - return FALSE - message_admins("[ADMIN_LOOKUPFLW(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[html_encode(message)]\"") - log_admin_private("[key_name(usr)] has passed the soft filter for \"[soft_filter_result[CHAT_FILTER_INDEX_WORD]]\" they may be using a disallowed term in PDA messages. Message: \"[message]\"") - - // Send the signal - var/list/string_targets = list() - for (var/obj/item/modular_computer/comp in targets) - if (comp.saved_identification && comp.saved_job) // != src is checked by the UI - string_targets += STRINGIFY_PDA_TARGET(comp.saved_identification, comp.saved_job) - - if (!string_targets.len) - return FALSE - var/sent_prob = 1 - if(ishuman(user)) - var/mob/living/carbon/human/old_person = user - sent_prob = old_person.age >= 30 ? 25 : sent_prob - if (computer && prob(sent_prob)) - message += " [computer.get_messenger_ending()]" - - var/datum/signal/subspace/messaging/tablet_msg/signal = new(computer, list( - "name" = fake_name || computer.saved_identification, - "job" = fake_job || computer.saved_job, - "message" = html_decode(message), - "ref" = REF(computer), - "targets" = targets, - "rigged" = rigged, - "photo" = saved_image, - "photo_path" = photo_path, - "automated" = FALSE, - )) - if(rigged) //Will skip the message server and go straight to the hub so it can't be cheesed by disabling the message server machine - signal.data["rigged_user"] = REF(user) // Used for bomb logging - signal.server_type = /obj/machinery/telecomms/hub - signal.data["reject"] = FALSE // Do not refuse the message - - signal.send_to_receivers() - - // If it didn't reach, note that fact - if (!signal.data["done"]) - to_chat(user, span_notice("ERROR: Server isn't responding.")) - if(ringer_status) - playsound(src, 'sound/machines/terminal_error.ogg', 15, TRUE) - return FALSE - - message = emoji_parse(message)//already sent- this just shows the sent emoji as one to the sender in the to_chat - signal.data["message"] = emoji_parse(signal.data["message"]) - - // Log it in our logs - var/list/message_data = list() - message_data["name"] = signal.data["name"] - message_data["job"] = signal.data["job"] - message_data["contents"] = html_decode(signal.format_message()) - message_data["outgoing"] = TRUE - message_data["ref"] = signal.data["ref"] - message_data["photo_path"] = signal.data["photo_path"] - message_data["photo"] = signal.data["photo"] - message_data["target_details"] = signal.format_target() - - // Show it to ghosts - var/ghost_message = span_name("[message_data["name"]] [rigged ? "Rigged" : ""] PDA Message --> [span_name("[signal.format_target()]")]: [signal.format_message()]") - for(var/mob/player_mob in GLOB.player_list) - if(player_mob.client && !player_mob.client?.prefs) - stack_trace("[player_mob] ([player_mob.ckey]) had null prefs, which shouldn't be possible!") - continue - - if(isobserver(player_mob) && (player_mob.client?.prefs.chat_toggles & CHAT_GHOSTPDA)) - to_chat(player_mob, "[FOLLOW_LINK(player_mob, user)] [ghost_message]") - - // Log in the talk log - user.log_talk(message, LOG_PDA, tag="[rigged ? "Rigged" : ""] PDA: [message_data["name"]] to [signal.format_target()]") - if(rigged) - log_bomber(user, "sent a rigged PDA message (Name: [message_data["name"]]. Job: [message_data["job"]]) to [english_list(string_targets)] [!is_special_character(user) ? "(SENT BY NON-ANTAG)" : ""]") - to_chat(user, span_info("PDA message sent to [signal.format_target()]: [signal.format_message()]")) - - if (ringer_status) - computer.send_sound() - - last_text = world.time - if (everyone) - message_data["name"] = "Everyone" - message_data["job"] = "" - last_text_everyone = world.time - - messages += list(message_data) - saved_image = null - photo_path = null - return TRUE - -/datum/computer_file/program/messenger/proc/receive_message(datum/signal/subspace/messaging/tablet_msg/signal) - var/list/message_data = list() - message_data["name"] = signal.data["name"] - message_data["job"] = signal.data["job"] - message_data["contents"] = html_decode(signal.format_message()) - message_data["outgoing"] = FALSE - message_data["ref"] = signal.data["ref"] - message_data["automated"] = signal.data["automated"] - message_data["photo_path"] = signal.data["photo_path"] - message_data["photo"] = signal.data["photo"] - messages += list(message_data) - - var/mob/living/L = null - if(computer.loc && isliving(computer.loc)) - L = computer.loc - //Maybe they are a pAI! - else - L = get(computer, /mob/living/silicon) - - if(L && (L.stat == CONSCIOUS || L.stat == SOFT_CRIT)) - var/reply = "(Reply)" - var/hrefstart - var/hrefend - if (isAI(L)) - hrefstart = "" - hrefend = "" - - if(signal.data["automated"]) - reply = "\[Automated Message\]" - - var/inbound_message = signal.format_message() - inbound_message = emoji_parse(inbound_message) - - if(L.is_literate()) - var/photo_message = message_data["photo"] ? " (Photo)" : "" - to_chat(L, span_infoplain("[icon2html(computer)] PDA message from [hrefstart][signal.data["name"]] ([signal.data["job"]])[hrefend], [inbound_message][photo_message] [reply]")) - - if (ringer_status) - computer.ring(ringtone) - -/// topic call that answers to people pressing "(Reply)" in chat -/datum/computer_file/program/messenger/Topic(href, href_list) - ..() - if(QDELETED(src)) - return - // send an activation message, open the messenger, kill whoever reads this nesting mess - if(!computer.enabled) - if(!computer.turn_on(usr, open_ui = FALSE)) - return - if(computer.active_program != src) - if(!computer.open_program(usr, src, open_ui = FALSE)) - return - if(!href_list["close"] && usr.can_perform_action(computer, FORBID_TELEKINESIS_REACH)) - switch(href_list["choice"]) - if("Message") - send_message(usr, list(locate(href_list["target"]))) - if("mess_us_up") - if(!HAS_TRAIT(src, TRAIT_PDA_CAN_EXPLODE)) - var/obj/item/modular_computer/pda/comp = computer - comp.explode(usr, from_message_menu = TRUE) - return diff --git a/code/modules/modular_computers/file_system/programs/wirecarp.dm b/code/modules/modular_computers/file_system/programs/wirecarp.dm index 9045fcc76b2..712d1e92cda 100644 --- a/code/modules/modular_computers/file_system/programs/wirecarp.dm +++ b/code/modules/modular_computers/file_system/programs/wirecarp.dm @@ -29,11 +29,10 @@ SSmodular_computers.purge_logs() return TRUE if("toggle_mass_pda") - var/obj/item/modular_computer/target_tablet = locate(params["ref"]) in GLOB.TabletMessengers - if(!istype(target_tablet)) + if(!(params["ref"] in GLOB.pda_messengers)) return - for(var/datum/computer_file/program/messenger/messenger_app in target_tablet.stored_files) - messenger_app.spam_mode = !messenger_app.spam_mode + var/datum/computer_file/program/messenger/target_messenger = GLOB.pda_messengers[params["ref"]] + target_messenger.spam_mode = !target_messenger.spam_mode return TRUE /datum/computer_file/program/ntnetmonitor/ui_data(mob/user) @@ -56,14 +55,14 @@ data["ntnetlogs"] += list(list("entry" = i)) data["tablets"] = list() - for(var/obj/item/modular_computer/messenger as anything in GetViewableDevices()) - var/list/tablet_data = list() - if(messenger.saved_identification) - for(var/datum/computer_file/program/messenger/messenger_app in computer.stored_files) - tablet_data["enabled_spam"] += messenger_app.spam_mode + for(var/messenger_ref in get_messengers_sorted_by_name()) + var/datum/computer_file/program/messenger/app = GLOB.pda_messengers[messenger_ref] + var/obj/item/modular_computer/pda = app.computer - tablet_data["name"] += messenger.saved_identification - tablet_data["ref"] += REF(messenger) + var/list/tablet_data = list() + tablet_data["enabled_spam"] = app.spam_mode + tablet_data["name"] = pda.saved_identification + tablet_data["ref"] = REF(app) data["tablets"] += list(tablet_data) diff --git a/tgstation.dme b/tgstation.dme index fe67d0a6b32..7ab9dac3e75 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -297,6 +297,7 @@ #include "code\__DEFINES\dcs\signals\signals_mind.dm" #include "code\__DEFINES\dcs\signals\signals_mining.dm" #include "code\__DEFINES\dcs\signals\signals_mod.dm" +#include "code\__DEFINES\dcs\signals\signals_modular_computer.dm" #include "code\__DEFINES\dcs\signals\signals_moveloop.dm" #include "code\__DEFINES\dcs\signals\signals_movetype.dm" #include "code\__DEFINES\dcs\signals\signals_music.dm" @@ -4495,6 +4496,7 @@ #include "code\modules\modular_computers\computers\machinery\modular_computer.dm" #include "code\modules\modular_computers\file_system\computer_file.dm" #include "code\modules\modular_computers\file_system\data.dm" +#include "code\modules\modular_computers\file_system\picture_file.dm" #include "code\modules\modular_computers\file_system\program.dm" #include "code\modules\modular_computers\file_system\programs\airestorer.dm" #include "code\modules\modular_computers\file_system\programs\alarm.dm" @@ -4514,7 +4516,6 @@ #include "code\modules\modular_computers\file_system\programs\notepad.dm" #include "code\modules\modular_computers\file_system\programs\nt_pay.dm" #include "code\modules\modular_computers\file_system\programs\ntdownloader.dm" -#include "code\modules\modular_computers\file_system\programs\ntmessenger.dm" #include "code\modules\modular_computers\file_system\programs\ntnrc_client.dm" #include "code\modules\modular_computers\file_system\programs\portrait_printer.dm" #include "code\modules\modular_computers\file_system\programs\powermonitor.dm" @@ -4537,6 +4538,8 @@ #include "code\modules\modular_computers\file_system\programs\maintenance\modsuit.dm" #include "code\modules\modular_computers\file_system\programs\maintenance\phys_scanner.dm" #include "code\modules\modular_computers\file_system\programs\maintenance\themes.dm" +#include "code\modules\modular_computers\file_system\programs\messenger\messenger_data.dm" +#include "code\modules\modular_computers\file_system\programs\messenger\messenger_program.dm" #include "code\modules\modular_computers\NTNet\NTNRC\conversation.dm" #include "code\modules\movespeed\_movespeed_modifier.dm" #include "code\modules\movespeed\modifiers\components.dm" diff --git a/tgui/packages/tgui/interfaces/AdminPDA.js b/tgui/packages/tgui/interfaces/AdminPDA.js index 9e620632607..c9a60dd6987 100644 --- a/tgui/packages/tgui/interfaces/AdminPDA.js +++ b/tgui/packages/tgui/interfaces/AdminPDA.js @@ -1,11 +1,11 @@ -import { useBackend, useLocalState } from '../backend'; import { Section, Dropdown, Input, Box, TextArea } from '../components'; -import { Button, ButtonCheckbox } from '../components/Button'; +import { useBackend, useLocalState } from '../backend'; +import { Button } from '../components/Button'; import { Window } from '../layouts'; export const AdminPDA = (props, context) => { return ( - + @@ -15,19 +15,32 @@ export const AdminPDA = (props, context) => { ); }; -export const ReceiverChoice = (props, context) => { - const { act, data } = useBackend(context); - const receivers = Array.from(data.users).sort(); +const ReceiverChoice = (props, context) => { + const { data } = useBackend(context); + const { users } = data; + const receivers = Array.from(Object.values(users)); const [user, setUser] = useLocalState(context, 'user', ''); const [spam, setSpam] = useLocalState(context, 'spam', false); + const [showInvisible, setShowInvisible] = useLocalState( + context, + 'showInvisible', + false + ); return (
showInvisible || !rcvr.invisible) + .map((rcvr) => ({ + displayText: rcvr.username, + value: rcvr.ref, + }))} width="275px" mb={1} onSelected={(value) => { @@ -36,15 +49,24 @@ export const ReceiverChoice = (props, context) => { /> - setSpam(!spam)}> - Should it be sent to everyone? - + setShowInvisible(!showInvisible)} + content="Include invisible?" + /> + setSpam(!spam)} + content="Should it be sent to everyone?" + />
); }; -export const SenderInfo = (props, context) => { +const SenderInfo = (props, context) => { const [name, setName] = useLocalState(context, 'name', ''); const [job, setJob] = useLocalState(context, 'job', ''); @@ -72,7 +94,7 @@ export const SenderInfo = (props, context) => { ); }; -export const MessageInput = (props, context) => { +const MessageInput = (props, context) => { const { act } = useBackend(context); const [user, setUser] = useLocalState(context, 'user', ''); @@ -80,9 +102,16 @@ export const MessageInput = (props, context) => { const [job, setJob] = useLocalState(context, 'job', ''); const [messageText, setMessageText] = useLocalState(context, 'message', ''); const [spam, setSpam] = useLocalState(context, 'spam', false); + const [force, setForce] = useLocalState(context, 'force', false); + const [showInvisible, setShowInvisible] = useLocalState( + context, + 'showInvisible', + false + ); - const tooltipText = function (name, job, message) { + const tooltipText = function (name, job, message, target) { let reasonList = []; + if (!target) reasonList.push('target'); if (!name) reasonList.push('name'); if (!job) reasonList.push('job'); if (!message) reasonList.push('message text'); @@ -104,11 +133,20 @@ export const MessageInput = (props, context) => { /> + setForce(!force)} + /> - - - )} - -
- - Detected Messengers - setSearchUser(value)} - mx={1} - ml={27} - /> -
-
- -
- - {users.length === 0 && 'No users found'} - {users.map((messenger) => ( - - ))} - - {!!canSpam && ( -
-
- {!owner && !isSilicon && } - - - ); -}; - -const MessageListScreen = (props, context) => { - const { act, data } = useBackend(context); - const { messages = [] } = data; - return ( - - - -
-
- {messages.map((message) => ( - -
- - {message.outgoing ? '(OUTGOING)' : '(INCOMING)'} - - {message.outgoing ? ( - {message.target_details} - ) : ( -
-
- {message.contents} - {!!message.photo && ( - - )} -
-
- ))} -
-
-
- ); -}; diff --git a/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx b/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx new file mode 100644 index 00000000000..e069126ee8b --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosMessenger/ChatScreen.tsx @@ -0,0 +1,430 @@ +import { Stack, Section, Button, Box, Input, Modal, Tooltip, Icon } from '../../components'; +import { Component, RefObject, createRef, SFC } from 'inferno'; +import { NtMessage, NtMessenger, NtPicture } from './types'; +import { BooleanLike } from 'common/react'; +import { useBackend } from '../../backend'; +import { decodeHtmlEntities } from 'common/string'; + +type ChatScreenProps = { + canReply: BooleanLike; + chatRef?: string; + isSilicon: BooleanLike; + messages: NtMessage[]; + recipient: NtMessenger; + selectedPhoto?: string; + sendingVirus: BooleanLike; + storedPhotos?: NtPicture[]; + unreads: number; +}; + +type ChatScreenState = { + canSend: boolean; + message: string; + previewingImage?: string; + selectingPhoto: boolean; +}; + +const READ_UNREADS_TIME_MS = 1000; +const SEND_COOLDOWN_MS = 1000; + +export class ChatScreen extends Component { + readUnreadsTimeout: NodeJS.Timeout | null = null; + scrollRef: RefObject; + + state: ChatScreenState = { + message: '', + selectingPhoto: false, + canSend: true, + }; + + constructor(props: ChatScreenProps) { + super(props); + + this.scrollRef = createRef(); + + this.scrollToBottom = this.scrollToBottom.bind(this); + this.handleMessageInput = this.handleMessageInput.bind(this); + this.handleSelectPicture = this.handleSelectPicture.bind(this); + this.handleSendMessage = this.handleSendMessage.bind(this); + this.trySetReadTimeout = this.trySetReadTimeout.bind(this); + this.tryClearReadTimeout = this.tryClearReadTimeout.bind(this); + this.clearUnreads = this.clearUnreads.bind(this); + } + + componentDidMount() { + this.scrollToBottom(); + this.trySetReadTimeout(); + } + + componentDidUpdate( + prevProps: ChatScreenProps, + _prevState: ChatScreenState, + _snapshot: any + ) { + if (prevProps.messages.length !== this.props.messages.length) { + this.scrollToBottom(); + this.trySetReadTimeout(); + } + } + + componentWillUnmount() { + if (!this.props.chatRef) { + return; + } + + const { act } = useBackend(this.context); + + this.tryClearReadTimeout(); + + act('PDA_saveMessageDraft', { + ref: this.props.chatRef, + message: this.state.message, + }); + } + + trySetReadTimeout() { + if (!this.props.chatRef) { + return; + } + const unreadMessages = this.props.unreads; + + this.tryClearReadTimeout(); + + if (unreadMessages > 0) { + this.readUnreadsTimeout = setTimeout(() => { + this.clearUnreads(); + this.readUnreadsTimeout = null; + }, READ_UNREADS_TIME_MS); + } + } + + tryClearReadTimeout() { + if (this.readUnreadsTimeout) { + clearTimeout(this.readUnreadsTimeout); + this.readUnreadsTimeout = null; + } + } + + clearUnreads() { + const { act } = useBackend(this.context); + + act('PDA_clearUnreads', { ref: this.props.chatRef }); + } + + scrollToBottom() { + const scroll = this.scrollRef.current; + if (scroll !== null) { + scroll.scrollTop = scroll.scrollHeight; + } + } + + handleSelectPicture() { + const { isSilicon } = this.props; + const { act } = useBackend(this.context); + if (isSilicon) { + act('PDA_siliconSelectPhoto'); + } else { + this.setState({ selectingPhoto: true }); + } + } + + handleSendMessage() { + if (this.state.message === '') { + return; + } + + const { act } = useBackend(this.context); + const { chatRef, recipient } = this.props; + + let ref = chatRef ? chatRef : recipient.ref; + + act('PDA_sendMessage', { + ref: ref, + message: this.state.message, + }); + + this.setState({ message: '', canSend: false }); + setTimeout(() => this.setState({ canSend: true }), SEND_COOLDOWN_MS); + } + + handleMessageInput(_: any, val: string) { + this.setState({ message: val }); + } + + render() { + const { act } = useBackend(this.context); + const { + canReply, + messages, + recipient, + chatRef, + selectedPhoto, + storedPhotos, + sendingVirus, + unreads, + } = this.props; + const { message, canSend, previewingImage, selectingPhoto } = this.state; + + let filteredMessages: JSX.Element[] = []; + + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + const isSwitch = !( + index === 0 || messages[index - 1].outgoing === message.outgoing + ); + + // this code shouldn't be reached if there's no chat + if (index === messages.length - unreads) { + filteredMessages.push(); + } + + filteredMessages.push( + + this.setState({ previewingImage: message.photo_path! }) + : undefined + } + /> + + ); + } + + let sendingBar: JSX.Element; + + if (!canReply) { + sendingBar = ( +
+ + You cannot reply to this user. + +
+ ); + } else if (selectingPhoto) { + const photos = storedPhotos!.map((photo) => ( + + + + )); + + sendingBar = ( +
+
+ ); + } else { + const attachmentButton = sendingVirus ? ( + + + )} + + + + + + {buttons} + + + + + ); + } + + return ( + +
+
+ + +
+ + {!!(messages.length > 0 && canReply) && ( + <> + + This is the beginning of your chat with {recipient.name}. + + + + )} + + {filteredMessages} + +
+
+ + {sendingBar} + {previewingImage && ( + +
this.setState({ previewingImage: undefined })} + /> + }> + +
+
+ )} +
+ ); + } +} + +type ChatMessageProps = { + outgoing: BooleanLike; + message: string; + everyone: BooleanLike; + timestamp: string; + photoPath?: string; + onPreviewImage?: () => void; +}; + +const ChatMessage = (props: ChatMessageProps) => { + const { message, everyone, outgoing, photoPath, timestamp, onPreviewImage } = + props; + + const displayMessage = decodeHtmlEntities(message); + + return ( + + + {displayMessage} + + + + + {!!everyone && ( + Sent to everyone + )} + {photoPath !== null && ( + + )} + + ); +}; + +const ChatDivider: SFC<{ mt: number }> = (props) => { + return ( + +
+ Unread Messages +
+ + ); +}; diff --git a/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx new file mode 100644 index 00000000000..1b227ab09df --- /dev/null +++ b/tgui/packages/tgui/interfaces/NtosMessenger/index.tsx @@ -0,0 +1,336 @@ +import { Box, Button, Icon, Section, Stack, Input, TextArea, Dimmer, Divider } from '../../components'; +import { useBackend, useLocalState } from '../../backend'; +import { createSearch } from 'common/string'; +import { BooleanLike } from 'common/react'; +import { NtosWindow } from '../../layouts'; + +import { NtChat, NtMessenger, NtPicture } from './types'; +import { ChatScreen } from './ChatScreen'; +import { sortBy } from 'common/collections'; + +type NtosMessengerData = { + can_spam: BooleanLike; + is_silicon: BooleanLike; + owner?: NtMessenger; + saved_chats: Record; + messengers: Record; + sort_by_job: BooleanLike; + alert_silenced: BooleanLike; + alert_able: BooleanLike; + sending_and_receiving: BooleanLike; + open_chat: string; + stored_photos?: NtPicture[]; + selected_photo_path?: string; + on_spam_cooldown: BooleanLike; + virus_attach: BooleanLike; + sending_virus: BooleanLike; +}; + +export const NtosMessenger = (props, context) => { + const { data } = useBackend(context); + const { + is_silicon, + saved_chats, + stored_photos, + selected_photo_path, + open_chat, + messengers, + sending_virus, + } = data; + + let content: JSX.Element; + if (open_chat !== null) { + const openChat = saved_chats[open_chat]; + const temporaryRecipient = messengers[open_chat]; + + content = ( + + ); + } else { + content = ; + } + + return ( + + {content} + + ); +}; + +const ContactsScreen = (props: any, context: any) => { + const { act, data } = useBackend(context); + const { + owner, + alert_silenced, + alert_able, + sending_and_receiving, + saved_chats, + messengers, + sort_by_job, + can_spam, + is_silicon, + virus_attach, + sending_virus, + } = data; + + const [searchUser, setSearchUser] = useLocalState(context, 'searchUser', ''); + + const sortByUnreads = sortBy((chat) => chat.unread_messages); + + const searchChatByName = createSearch( + searchUser, + (chat: NtChat) => chat.recipient.name + chat.recipient.job + ); + const searchMessengerByName = createSearch( + searchUser, + (messenger: NtMessenger) => messenger.name + messenger.job + ); + + const chatToButton = (chat: NtChat) => { + return ( + + ); + }; + + const messengerToButton = (messenger: NtMessenger) => { + return ( + + ); + }; + + const openChatsArray = sortByUnreads(Object.values(saved_chats)).filter( + searchChatByName + ); + + const filteredChatButtons = openChatsArray + .filter((c) => c.visible) + .map(chatToButton); + + const messengerButtons = Object.entries(messengers) + .filter( + ([ref, messenger]) => + openChatsArray.every((chat) => chat.recipient.ref !== ref) && + searchMessengerByName(messenger) + ) + .map(([_, messenger]) => messenger) + .map(messengerToButton) + .concat(openChatsArray.filter((chat) => !chat.visible).map(chatToButton)); + + const noId = !owner && !is_silicon; + + return ( + + +
+ + + + SpaceMessenger V6.5.3 + + + Bringing you spy-proof communications since 2467. + +
+
+ {filteredChatButtons.length > 0 && ( + + +
+ + Previous Messages +
+
+ {filteredChatButtons} +
+
+
+ )} + + +
+ + + + Detected Messengers + + +
+
+ + {messengerButtons.length === 0 && ( + + + + No users found. + + + )} + {messengerButtons} + +
+
+
+ {!!can_spam && ( + + + + )} + {noId && } +
+ ); +}; + +type ChatButtonProps = { + name: string; + unreads: number; + chatRef: string; +}; + +const ChatButton = (props: ChatButtonProps, context) => { + const { act } = useBackend(context); + const unreadMessages = props.unreads; + const hasUnreads = unreadMessages > 0; + return ( + + ); +}; + +const SendToAllSection = (props, context) => { + const { data, act } = useBackend(context); + const { on_spam_cooldown } = data; + + const [message, setmessage] = useLocalState(context, 'everyoneMessage', ''); + + return ( + <> +
+ + + + Send To All + + + + + +
+
+