diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index e9d813c1439..caa051f8748 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -88,7 +88,9 @@
//get message text, limit it's length.and clean/escape html
if(!msg)
+ set_typing(C, TRUE)
msg = input(src,"Message:", "Private message to [holder ? key_name(C, FALSE) : key_name_hidden(C, FALSE)]") as text|null
+ set_typing(C, FALSE)
if(!msg)
return
@@ -130,6 +132,9 @@
var/recieve_message = ""
+ pm_tracker.add_message(C, src, msg, mob)
+ C.pm_tracker.add_message(src, src, msg, C.mob)
+
if(holder && !C.holder)
recieve_message = "-- Click the [recieve_pm_type]'s name to reply --\n"
if(C.adminhelped)
@@ -233,3 +238,141 @@
continue
if(check_rights(R_ADMIN|R_MOD|R_MENTOR, 0, X.mob))
to_chat(X, "PM: [key_name(src, TRUE, 0)]->IRC-Admins: [msg]")
+
+/client/verb/open_pms_ui()
+ set name = "My PMs"
+ set category = "OOC"
+ pm_tracker.show_ui(usr)
+
+/client/proc/set_typing(client/target, value)
+ if(!target)
+ return
+ var/datum/pm_convo/convo = target.pm_tracker.pms[key]
+ if(!convo)
+ return
+ convo.typing = value
+ if(target.pm_tracker.open && target.pm_tracker.current_title == key)
+ target.pm_tracker.show_ui(target.mob)
+
+/datum/pm_tracker
+ var/current_title = ""
+ var/open = FALSE
+ var/list/pms = list()
+ var/show_archived = FALSE
+ var/window_id = "pms_window"
+
+/datum/pm_convo
+ var/list/messages = list()
+ var/archived = FALSE
+ var/client/client
+ var/read = FALSE
+ var/typing = FALSE
+
+/datum/pm_convo/New(client/C)
+ client = C
+
+/datum/pm_convo/proc/add(client/sender, message)
+ messages.Add("[sender]: [message]")
+ archived = FALSE
+ read = FALSE
+
+/datum/pm_tracker/proc/add_message(client/title, client/sender, message, mob/user)
+ if(!pms[title.key])
+ pms[title.key] = new /datum/pm_convo(title)
+ pms[title.key].add(sender, message)
+
+ if(!open)
+ // The next time the window's opened, it'll be open to the most recent message
+ current_title = title.key
+ return
+
+ // If it's already opened, it'll refresh
+ show_ui(user)
+
+/datum/pm_tracker/proc/show_ui(mob/user)
+ var/dat = ""
+
+ dat += "Refresh"
+ dat += "[show_archived ? "Hide" : "Show"] Archived"
+ dat += "
"
+ for(var/title in pms)
+ if(pms[title].archived && !show_archived)
+ continue
+ var/label = "[title]"
+ var/class = ""
+ if(title == current_title)
+ label = "[label]"
+ class = "linkOn"
+ else if(!pms[title].read)
+ label = "*[label]"
+ dat += "[label]"
+
+ var/datum/pm_convo/convo = pms[current_title]
+ if(convo)
+ convo.read = TRUE
+ dat += "
[check_rights(R_ADMIN, FALSE, user) ? fancy_title(current_title) : current_title]
"
+ dat += ""
+ dat += "
"
+
+ for(var/message in convo.messages)
+ dat += "| [message] |
"
+
+ dat += "
"
+ if(convo.typing)
+ dat += "[current_title] is typing"
+ dat += "
"
+ dat += ""
+ dat += "Reply"
+ dat += "[convo.archived ? "Unarchive" : "Archive"]"
+ if(check_rights(R_ADMIN, FALSE, user))
+ dat += "Ping"
+
+ var/datum/browser/popup = new(user, window_id, "Messages", 1000, 600, src)
+ popup.set_content(dat)
+ popup.open()
+ open = TRUE
+
+/datum/pm_tracker/proc/fancy_title(title)
+ var/client/C = pms[title].client
+ if(!C)
+ return "[title] (Disconnected)"
+ return "[key_name(C, FALSE)] ([ADMIN_QUE(C.mob,"?")]) ([ADMIN_PP(C.mob,"PP")]) ([ADMIN_VV(C.mob,"VV")]) ([ADMIN_SM(C.mob,"SM")]) ([admin_jump_link(C.mob)]) (CA)"
+
+/datum/pm_tracker/Topic(href, href_list)
+ if(href_list["archive"])
+ pms[href_list["archive"]].archived = !pms[href_list["archive"]].archived
+ show_ui(usr)
+ return
+
+ if(href_list["refresh"])
+ show_ui(usr)
+ return
+
+ if(href_list["newtitle"])
+ current_title = href_list["newtitle"]
+ show_ui(usr)
+ return
+
+ if(href_list["ping"])
+ var/client/C = pms[href_list["ping"]].client
+ if(C)
+ C.pm_tracker.current_title = usr.key
+ window_flash(C)
+ C.pm_tracker.show_ui(C.mob)
+ to_chat(usr, "Forced open [C]'s messages window.")
+ show_ui(usr)
+ return
+
+ if(href_list["reply"])
+ usr.client.cmd_admin_pm(ckey(href_list["reply"]), null)
+ show_ui(usr)
+ return
+
+ if(href_list["showarchived"])
+ show_archived = !show_archived
+ show_ui(usr)
+ return
+
+ if(href_list["close"])
+ open = FALSE
+ return
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index c15ea50c93e..a6bc558842b 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -7,6 +7,7 @@
var/last_message = "" //contains the last message sent by this client - used to protect against copy-paste spamming.
var/last_message_count = 0 //contains a number of how many times a message identical to last_message was sent.
var/last_message_time = 0 //holds the last time (based on world.time) a message was sent
+ var/datum/pm_tracker/pm_tracker = new()
/////////
//OTHER//
diff --git a/html/browser/common.css b/html/browser/common.css
index 23b675f7fa2..3480183d129 100644
--- a/html/browser/common.css
+++ b/html/browser/common.css
@@ -345,4 +345,22 @@ div.notice
-ms-interpolation-mode: nearest-neighbor;
width: 64px;
height:64px;
-}
\ No newline at end of file
+}
+
+.typing:after
+{
+ overflow: hidden;
+ display: inline-block;
+ vertical-align: bottom;
+ animation: ellipsis steps(4,end) 900ms infinite;
+ content: "\2026";
+ width: 0px;
+}
+
+@keyframes ellipsis
+{
+ to
+ {
+ width: 1.25em;
+ }
+}