diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index 28841f3d5ba..337399c8dde 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -98,6 +98,10 @@
CtrlClickOn(A)
return
+ //SKYRAT EDIT ADDITION BEGIN - TYPING_INDICATOR
+ if(typing_indicator)
+ set_typing_indicator(FALSE)
+ //SKYRAT EDIT ADDITION END
if(incapacitated(ignore_restraints = TRUE, ignore_stasis = TRUE))
return
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index 541fa717a43..e26d8ccba2d 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -327,7 +327,8 @@ GLOBAL_LIST_INIT(department_radio_keys, list(
speech_bubble_recipients.Add(M.client)
var/image/I = image('icons/mob/talk.dmi', src, "[bubble_type][say_test(message)]", FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
- INVOKE_ASYNC(GLOBAL_PROC, /.proc/flick_overlay, I, speech_bubble_recipients, 30)
+ //INVOKE_ASYNC(GLOBAL_PROC, /.proc/flick_overlay, I, speech_bubble_recipients, 30) - ORIGINAL
+ INVOKE_ASYNC(GLOBAL_PROC, /.proc/animate_speechbubble, I, speech_bubble_recipients, 30) //SKYRAT EDIT CHANGE - TYPING_INDICATOR
/mob/proc/binarycheck()
return FALSE
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 56749f30378..db7238c2207 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -4,6 +4,10 @@
/mob/verb/say_verb(message as text)
set name = "Say"
set category = "IC"
+ //SKYRAT EDIT ADDITION BEGIN - TYPING_INDICATOR
+ if(typing_indicator)
+ set_typing_indicator(FALSE)
+ //SKYRAT EDIT ADDITION END
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, "Speech is currently admin-disabled.")
return
@@ -27,6 +31,10 @@
/mob/verb/me_verb(message as text)
set name = "Me"
set category = "IC"
+ //SKYRAT EDIT ADDITION BEGIN - TYPING_INDICATOR
+ if(typing_indicator)
+ set_typing_indicator(FALSE)
+ //SKYRAT EDIT ADDITION END
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, "Speech is currently admin-disabled.")
diff --git a/modular_skyrat/modules/typing_indicator/code/mob.dm b/modular_skyrat/modules/typing_indicator/code/mob.dm
new file mode 100644
index 00000000000..37d332cb077
--- /dev/null
+++ b/modular_skyrat/modules/typing_indicator/code/mob.dm
@@ -0,0 +1,34 @@
+GLOBAL_VAR_INIT(typing_indicator_overlay, mutable_appearance('modular_skyrat/modules/typing_indicator/icons/typing_indicator.dmi', "default0", FLY_LAYER))
+
+/mob
+ var/typing_indicator = FALSE
+
+/mob/proc/set_typing_indicator(var/state)
+ typing_indicator = state
+ if(typing_indicator)
+ add_overlay(GLOB.typing_indicator_overlay)
+ else
+ cut_overlay(GLOB.typing_indicator_overlay)
+
+/mob/living/key_down(_key, client/user)
+ if(!typing_indicator && stat == CONSCIOUS)
+ switch(_key)
+ if("T")
+ set_typing_indicator(TRUE)
+ if("M")
+ set_typing_indicator(TRUE)
+ return ..()
+
+/proc/animate_speechbubble(image/I, list/show_to, duration)
+ var/matrix/M = matrix()
+ M.Scale(0,0)
+ I.transform = M
+ I.alpha = 0
+ for(var/client/C in show_to)
+ C.images += I
+ animate(I, transform = 0, alpha = 255, time = 5, easing = ELASTIC_EASING)
+ sleep(duration-5)
+ animate(I, alpha = 0, time = 5, easing = EASE_IN)
+ sleep(20)
+ for(var/client/C in show_to)
+ C.images -= I
diff --git a/modular_skyrat/modules/typing_indicator/icons/typing_indicator.dmi b/modular_skyrat/modules/typing_indicator/icons/typing_indicator.dmi
new file mode 100644
index 00000000000..4a6351e53fe
Binary files /dev/null and b/modular_skyrat/modules/typing_indicator/icons/typing_indicator.dmi differ
diff --git a/modular_skyrat/modules/typing_indicator/readme.md b/modular_skyrat/modules/typing_indicator/readme.md
new file mode 100644
index 00000000000..f3ea6bcaa6a
--- /dev/null
+++ b/modular_skyrat/modules/typing_indicator/readme.md
@@ -0,0 +1,27 @@
+## Title: Typing Indicator
+
+MODULE ID: TYPING_INDICATOR
+
+### Description:
+
+Adds an asynchronously tracked typing indicator, which shows a typing bubble with an animation.
+Added in a hacky way to make it very smooth on the user's end. It registers "T" and "M" keypresses for the indicator to popup, and makes it gone on mouse click, or say/emote send
+
+### TG Proc Changes:
+
+ - APPEND: code/modules/keybindings/setup.dm > /datum/proc/key_down()
+ - ADDITION: code/onclick/_click.dm > /mob/proc/ClickOn()
+ - ADDITION: code/modules/mob/mob_say.dm > /mob/verb/say_verb(), /mob/verb/me_verb()
+ - CHANGE: code\modules\mob\living\living_say.dm > /mob/living/send_speech
+
+### Defines:
+
+N/A
+
+### Included files:
+
+N/A
+
+### Credits:
+
+Azarak
diff --git a/tgstation.dme b/tgstation.dme
index d1872a02477..8fb6621bcd1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3380,4 +3380,5 @@
#include "modular_skyrat\modules\icspawning\code\game\objects\items\cards_ids.dm"
#include "modular_skyrat\modules\icspawning\code\modules\clothing\outfits\standard.dm"
#include "modular_skyrat\modules\icspawning\code\modules\spells\spell.dm"
+#include "modular_skyrat\modules\typing_indicator\code\mob.dm"
// END_INCLUDE