diff --git a/code/game/objects/items/devices/communicator/UI.dm b/code/game/objects/items/devices/communicator/UI.dm
new file mode 100644
index 00000000000..867d7956ef5
--- /dev/null
+++ b/code/game/objects/items/devices/communicator/UI.dm
@@ -0,0 +1,277 @@
+// Proc: ui_interact()
+// Parameters: 4 (standard NanoUI arguments)
+// Description: Uses a bunch of for loops to turn lists into lists of lists, so they can be displayed in nanoUI, then displays various buttons to the user.
+/obj/item/device/communicator/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/key_state = null)
+ // this is the data which will be sent to the ui
+ var/data[0] //General nanoUI information
+ var/communicators[0] //List of communicators
+ var/invites[0] //Communicators and ghosts we've invited to our communicator.
+ var/requests[0] //Communicators and ghosts wanting to go in our communicator.
+ var/voices[0] //Current /mob/living/voice s inside the device.
+ var/connected_communicators[0] //Current communicators connected to the device.
+
+ var/im_contacts_ui[0] //List of communicators that have been messaged.
+ var/im_list_ui[0] //List of messages.
+
+ var/weather[0]
+ var/modules_ui[0] //Home screen info.
+
+ //First we add other 'local' communicators.
+ for(var/obj/item/device/communicator/comm in known_devices)
+ if(comm.network_visibility && comm.exonet)
+ communicators[++communicators.len] = list("name" = sanitize(comm.name), "address" = comm.exonet.address)
+
+ //Now for ghosts who we pretend have communicators.
+ for(var/mob/observer/dead/O in known_devices)
+ if(O.client && O.client.prefs.communicator_visibility == 1 && O.exonet)
+ communicators[++communicators.len] = list("name" = sanitize("[O.client.prefs.real_name]'s communicator"), "address" = O.exonet.address, "ref" = "\ref[O]")
+
+ //Lists all the other communicators that we invited.
+ for(var/obj/item/device/communicator/comm in voice_invites)
+ if(comm.exonet)
+ invites[++invites.len] = list("name" = sanitize(comm.name), "address" = comm.exonet.address, "ref" = "\ref[comm]")
+
+ //Ghosts we invited.
+ for(var/mob/observer/dead/O in voice_invites)
+ if(O.exonet && O.client)
+ invites[++invites.len] = list("name" = sanitize("[O.client.prefs.real_name]'s communicator"), "address" = O.exonet.address, "ref" = "\ref[O]")
+
+ //Communicators that want to talk to us.
+ for(var/obj/item/device/communicator/comm in voice_requests)
+ if(comm.exonet)
+ requests[++requests.len] = list("name" = sanitize(comm.name), "address" = comm.exonet.address, "ref" = "\ref[comm]")
+
+ //Ghosts that want to talk to us.
+ for(var/mob/observer/dead/O in voice_requests)
+ if(O.exonet && O.client)
+ requests[++requests.len] = list("name" = sanitize("[O.client.prefs.real_name]'s communicator"), "address" = O.exonet.address, "ref" = "\ref[O]")
+
+ //Now for all the voice mobs inside the communicator.
+ for(var/mob/living/voice/voice in contents)
+ voices[++voices.len] = list("name" = sanitize("[voice.name]'s communicator"), "true_name" = sanitize(voice.name))
+
+ //Finally, all the communicators linked to this one.
+ for(var/obj/item/device/communicator/comm in communicating)
+ connected_communicators[++connected_communicators.len] = list("name" = sanitize(comm.name), "true_name" = sanitize(comm.name), "ref" = "\ref[comm]")
+
+ //Devices that have been messaged or recieved messages from.
+ for(var/obj/item/device/communicator/comm in im_contacts)
+ if(comm.exonet)
+ im_contacts_ui[++im_contacts_ui.len] = list("name" = sanitize(comm.name), "address" = comm.exonet.address, "ref" = "\ref[comm]")
+
+ for(var/mob/observer/dead/ghost in im_contacts)
+ if(ghost.exonet)
+ im_contacts_ui[++im_contacts_ui.len] = list("name" = sanitize(ghost.name), "address" = ghost.exonet.address, "ref" = "\ref[ghost]")
+
+ for(var/obj/item/integrated_circuit/input/EPv2/CIRC in im_contacts)
+ if(CIRC.exonet && CIRC.assembly)
+ im_contacts_ui[++im_contacts_ui.len] = list("name" = sanitize(CIRC.assembly.name), "address" = CIRC.exonet.address, "ref" = "\ref[CIRC]")
+
+
+ //Actual messages.
+ for(var/I in im_list)
+ im_list_ui[++im_list_ui.len] = list("address" = I["address"], "to_address" = I["to_address"], "im" = I["im"])
+
+ //Weather reports.
+ for(var/datum/planet/planet in SSplanets.planets)
+ if(planet.weather_holder && planet.weather_holder.current_weather)
+ var/list/W = list(
+ "Planet" = planet.name,
+ "Time" = planet.current_time.show_time("hh:mm"),
+ "Weather" = planet.weather_holder.current_weather.name,
+ "Temperature" = planet.weather_holder.temperature - T0C,
+ "High" = planet.weather_holder.current_weather.temp_high - T0C,
+ "Low" = planet.weather_holder.current_weather.temp_low - T0C,
+ "WindDir" = planet.weather_holder.wind_dir ? dir2text(planet.weather_holder.wind_dir) : "None",
+ "WindSpeed" = planet.weather_holder.wind_speed ? "[planet.weather_holder.wind_speed > 2 ? "Severe" : "Normal"]" : "None",
+ "Forecast" = english_list(planet.weather_holder.forecast, and_text = "→", comma_text = "→", final_comma_text = "→") // Unicode RIGHTWARDS ARROW.
+ )
+ weather[++weather.len] = W
+
+ // Update manifest
+ data_core.get_manifest_list()
+
+ //Modules for homescreen.
+ for(var/list/R in modules)
+ modules_ui[++modules_ui.len] = R
+
+ data["user"] = "\ref[user]" // For receiving input() via topic, because input(usr,...) wasn't working on cartridges
+ data["owner"] = owner ? owner : "Unset"
+ data["occupation"] = occupation ? occupation : "Swipe ID to set."
+ data["connectionStatus"] = get_connection_to_tcomms()
+ data["visible"] = network_visibility
+ data["address"] = exonet.address ? exonet.address : "Unallocated"
+ data["targetAddress"] = target_address
+ data["targetAddressName"] = target_address_name
+ data["currentTab"] = selected_tab
+ data["knownDevices"] = communicators
+ data["invitesSent"] = invites
+ data["requestsReceived"] = requests
+ data["voice_mobs"] = voices
+ data["communicating"] = connected_communicators
+ data["video_comm"] = video_source ? "\ref[video_source.loc]" : null
+ data["imContacts"] = im_contacts_ui
+ data["imList"] = im_list_ui
+ data["time"] = stationtime2text()
+ data["ring"] = ringer
+ data["homeScreen"] = modules_ui
+ data["note"] = note // current notes
+ data["weather"] = weather
+ data["aircontents"] = src.analyze_air()
+ data["flashlight"] = fon
+ data["manifest"] = PDA_Manifest
+ data["feeds"] = compile_news()
+ data["latest_news"] = get_recent_news()
+ if(newsfeed_channel)
+ data["target_feed"] = data["feeds"][newsfeed_channel]
+
+ // update the ui if it exists, returns null if no ui is passed/found
+ ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open)
+ if(!ui)
+ // the ui does not exist, so we'll create a new() one
+ // for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm
+ data["currentTab"] = 1 // Reset the current tab, because we're going to home page
+ ui = new(user, src, ui_key, "communicator_header.tmpl", "Communicator", 475, 700, state = key_state)
+ // add templates for screens in common with communicator.
+ ui.add_template("atmosphericScan", "atmospheric_scan.tmpl")
+ ui.add_template("crewManifest", "crew_manifest.tmpl")
+ ui.add_template("Body", "communicator.tmpl") // Main body
+ // when the ui is first opened this is the data it will use
+ ui.set_initial_data(data)
+ // open the new ui window
+ ui.open()
+ // auto update every five Master Controller tick
+ ui.set_auto_update(5)
+
+// Proc: Topic()
+// Parameters: 2 (standard Topic arguments)
+// Description: Responds to NanoUI button presses.
+/obj/item/device/communicator/Topic(href, href_list)
+ if(..())
+ return 1
+ if(href_list["rename"])
+ var/new_name = sanitizeSafe(input(usr,"Please enter your name.","Communicator",usr.name) )
+ if(new_name)
+ register_device(new_name)
+
+ if(href_list["toggle_visibility"])
+ switch(network_visibility)
+ if(1) //Visible, becoming invisbile
+ network_visibility = 0
+ if(camera)
+ camera.remove_network(NETWORK_COMMUNICATORS)
+ if(0) //Invisible, becoming visible
+ network_visibility = 1
+ if(camera)
+ camera.add_network(NETWORK_COMMUNICATORS)
+
+ if(href_list["toggle_ringer"])
+ ringer = !ringer
+
+ if(href_list["add_hex"])
+ var/hex = href_list["add_hex"]
+ add_to_EPv2(hex)
+
+ if(href_list["write_target_address"])
+ var/new_address = sanitizeSafe(input(usr,"Please enter the desired target EPv2 address. Note that you must write the colons \
+ yourself.","Communicator",src.target_address) )
+ if(new_address)
+ target_address = new_address
+
+ if(href_list["clear_target_address"])
+ target_address = ""
+
+ if(href_list["dial"])
+ if(!get_connection_to_tcomms())
+ to_chat(usr, "Error: Cannot connect to Exonet node.")
+ return
+ var/their_address = href_list["dial"]
+ exonet.send_message(their_address, "voice")
+
+ if(href_list["decline"])
+ var/ref_to_remove = href_list["decline"]
+ var/atom/decline = locate(ref_to_remove)
+ if(decline)
+ del_request(decline)
+
+ if(href_list["message"])
+ if(!get_connection_to_tcomms())
+ to_chat(usr, "Error: Cannot connect to Exonet node.")
+ return
+ var/their_address = href_list["message"]
+ var/text = sanitizeSafe(input(usr,"Enter your message.","Text Message"))
+ if(text)
+ exonet.send_message(their_address, "text", text)
+ im_list += list(list("address" = exonet.address, "to_address" = their_address, "im" = text))
+ log_pda("(COMM: [src]) sent \"[text]\" to [exonet.get_atom_from_address(their_address)]", usr)
+ for(var/mob/M in player_list)
+ if(M.stat == DEAD && M.is_preference_enabled(/datum/client_preference/ghost_ears))
+ if(istype(M, /mob/new_player) || M.forbid_seeing_deadchat)
+ continue
+ if(exonet.get_atom_from_address(their_address) == M)
+ continue
+ M.show_message("Comm IM - [src] -> [exonet.get_atom_from_address(their_address)]: [text]")
+
+ if(href_list["disconnect"])
+ var/name_to_disconnect = href_list["disconnect"]
+ for(var/mob/living/voice/V in contents)
+ if(name_to_disconnect == V.name)
+ close_connection(usr, V, "[usr] hung up")
+ for(var/obj/item/device/communicator/comm in communicating)
+ if(name_to_disconnect == comm.name)
+ close_connection(usr, comm, "[usr] hung up")
+
+ if(href_list["startvideo"])
+ var/ref_to_video = href_list["startvideo"]
+ var/obj/item/device/communicator/comm = locate(ref_to_video)
+ if(comm)
+ connect_video(usr, comm)
+
+ if(href_list["endvideo"])
+ if(video_source)
+ end_video()
+
+ if(href_list["watchvideo"])
+ if(video_source)
+ watch_video(usr,video_source.loc)
+
+ if(href_list["copy"])
+ target_address = href_list["copy"]
+
+ if(href_list["copy_name"])
+ target_address_name = href_list["copy_name"]
+
+ if(href_list["hang_up"])
+ for(var/mob/living/voice/V in contents)
+ close_connection(usr, V, "[usr] hung up")
+ for(var/obj/item/device/communicator/comm in communicating)
+ close_connection(usr, comm, "[usr] hung up")
+
+ if(href_list["switch_tab"])
+ selected_tab = href_list["switch_tab"]
+
+ if(href_list["edit"])
+ var/n = input(usr, "Please enter message", name, notehtml)
+ n = sanitizeSafe(n, extra = 0)
+ if(n)
+ note = html_decode(n)
+ notehtml = note
+ note = replacetext(note, "\n", "
")
+ else
+ note = ""
+ notehtml = note
+
+ if(href_list["switch_template"])
+ var/datum/nanoui/ui = SSnanoui.get_open_ui(usr, src, "main")
+ if(ui)
+ ui.add_template("Body", href_list["switch_template"])
+
+ if(href_list["Light"])
+ fon = !fon
+ set_light(fon * flum)
+
+ if(href_list["newsfeed"])
+ newsfeed_channel = text2num(href_list["newsfeed"])
+
+ SSnanoui.update_uis(src)
+ add_fingerprint(usr)