diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index ee60643059..01a6b76903 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -25,8 +25,9 @@ from codecs import lookup as codecs_lookup from django.conf import settings -from evennia.accounts.models import AccountDB +from evennia import ObjectDB from evennia.commands.cmdhandler import cmdhandler +from evennia.commands.default.general import CmdSay from evennia.utils.logger import log_err from evennia.utils.utils import to_str @@ -650,6 +651,26 @@ def msdp_send(session, *args, **kwargs): session.msg(send=((), out)) +def is_typing_get_aliases(session, *args, **kwargs): + session.msg(is_typing={'type': 'aliases', 'payload': CmdSay.aliases}) + + +def is_typing_state(session, *args, **kwargs): + # audience = ObjectDB.objects.filter(db_typeclass_path="typeclasses.characters.Character", + # db_location=session.puppet.location).exclude(db_key=session.puppet.key) + + audience = ObjectDB.objects.filter(db_typeclass_path="typeclasses.characters.Character", + db_location=session.puppet.location) + + for puppet in audience: + for puppet_session in puppet.sessions.all(): + puppet_session.msg(is_typing={'type': 'typing', + 'payload': { + 'name': session.puppet.name, + 'state': args[0] + }}) + + # client specific diff --git a/evennia/web/static/webclient/css/webclient.css b/evennia/web/static/webclient/css/webclient.css index 55135acc60..66433c4b9b 100644 --- a/evennia/web/static/webclient/css/webclient.css +++ b/evennia/web/static/webclient/css/webclient.css @@ -346,6 +346,33 @@ div {margin:0px;} text-decoration: underline; } +#istyping { + position: fixed; + top: 25px; + right: 25px; + width: auto; + height: auto; + display: none; + color: white; +} + +.player-is-typing { + animation: isTyping 2s ease 0s infinite normal forwards; +} +@keyframes isTyping { + 0% { + opacity: 1; + } + + 50% { + opacity: 0.2; + } + + 100% { + opacity: 1; + } +} + /* XTERM256 colors */ diff --git a/evennia/web/static/webclient/js/plugins/is_typing.js b/evennia/web/static/webclient/js/plugins/is_typing.js new file mode 100644 index 0000000000..95ae44b584 --- /dev/null +++ b/evennia/web/static/webclient/js/plugins/is_typing.js @@ -0,0 +1,208 @@ +let is_typing = (function (){ + let Evennia; + // 10 second timeout + const timeout = 10 * 1000 + const state = { + timeout: null, + callback: null, + is_typing: false, + typing_players: [], + cleanup_callback: null, + } + + const sayCommands = ['say'] + + const createDialog = function() { + const ele =[ + '
', + '
', + '
' + ].join('\n') + + $('body').append(ele) + } + + const playerElement =(name)=> `
${name} is typing...
` + + const startedTyping = function () { + state.is_typing = true; + state.timeout = Date.now() + timeout; + state.callback = setTimeout(stoppedTyping, timeout); + + sendIsTyping() + } + + const stillTyping = function () { + state.timeout = Date.now() + timeout + clearTimeout(state.callback) + state.callback = setTimeout(stoppedTyping, timeout) + + sendIsTyping() + } + + const stoppedTyping = function () { + state.is_typing = false; + clearTimeout(state.callback); + state.callback = null; + state.timeout = null; + + sendIsTyping() + } + + // Make our commands array regex safe + const escapeRegExp = function (text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + } + + // Get the say command's aliases + const requestSayAliases = function () { + Evennia.msg('is_typing_get_aliases') + } + + const setSayAliases = function (aliases) { + aliases.forEach(alias=>sayCommands.push(escapeRegExp(alias))) + } + + // Update server + const sendIsTyping = function () { + Evennia.msg('is_typing_state', [state.is_typing]) + } + + const onLoggedIn = function () { + requestSayAliases(); + } + + // Listen for talk commands + const onKeydown = function (event) { + const regex = new RegExp(`^\W*(${sayCommands.reduce((acc, cur)=> acc + "|" + cur, "").substring(1)})`) + const inputfield = $(".inputfield:focus"); + + // A 'say' command is being used. + if (Evennia.isConnected() && + inputfield.length === 1 && + inputfield.val().match(regex)) { + // Enter. Message sent. Reset. + if (event.which === 13) { + stoppedTyping() + + // Speaking just started. Set is_talking and timeout. + } else if (!state.is_typing) { + startedTyping(); + + // Expiration is nearing. Update timeout. + } else if (Date.now() + 5 * 1000 > state.timeout) { + stillTyping(); + + } + // Not talking anymore but state hasn't been updated yet. + } else if (state.is_typing) { + stoppedTyping(); + } + } + + // Reset everything + const onConnectionClose = function () { + state.is_typing = false; + state.timeout = null; + state.typing_players = [] + + if (state.callback) { + clearTimeout(state.callback) + } + + if (state.cleanup_callback) { + clearInterval(state.cleanup_callback) + } + } + + const cleanupTimedOutPlayers = function () { + const now = Date.now(); + const timedOut = [] + + state.typing_players.forEach((player, index)=>{ + if (player.timeout < now) { + timedOut.push(index) + $(`#istyping-${player}`).remove() + } + }) + + timedOut.reverse().forEach(index=>state.typing_players.splice(index, 1)) + + if (state.typing_players.length === 0) { + clearInterval(state.cleanup_callback) + $('#istyping').hide(); + } + } + + const is_typing = function (args, kwargs) { + if ('type' in kwargs) { + switch (kwargs.type) { + case 'aliases': + setSayAliases(kwargs.payload) + break; + + case 'typing': + const player = state.typing_players.filter(player=>player.name === kwargs.payload.name) + + // New talker + if (kwargs.payload.state && + player.length === 0) { + state.typing_players.push({name: kwargs.payload.name, timeout: Date.now() + timeout}) + $('#typingplayers').append(playerElement(kwargs.payload.name)) + + // Existing talker is still going + } else if (kwargs.payload.state && + player.length > 0) { + player[0].timeout = Date.now() + timeout; + + // They're done talking + } else { + state.typing_players = state.typing_players.filter(player=>player.name!== kwargs.payload.name) + $(`#istyping-${kwargs.payload.name}`).remove() + } + + if (state.typing_players.length > 0 && !state.cleanup_callback) { + state.cleanup_callback = setInterval(cleanupTimedOutPlayers, 100); + $('#istyping').show(); + + } else if (state.typing_players.length === 0 && state.cleanup_callback) { + clearInterval(state.cleanup_callback) + state.cleanup_callback = null; + $('#istyping').hide(); + } + break; + + default: + console.log("Default case") + console.log(args) + console.log(kwargs) + } + } + } + + const getState = () => state + + // + // Mandatory plugin init function + const init = function () { + let options = window.options; + options["is_typing"] = true; + Evennia = window.Evennia; + + Evennia.emitter.on("is_typing", is_typing); + + createDialog(); + + console.log('Is Typing plugin initialized'); + } + + return { + init, + onLoggedIn, + onKeydown, + onConnectionClose, + getState + } +})() + +window.plugin_handler.add("is_typing", is_typing) \ No newline at end of file diff --git a/evennia/web/templates/webclient/base.html b/evennia/web/templates/webclient/base.html index a76c45f801..7cf5a4be23 100644 --- a/evennia/web/templates/webclient/base.html +++ b/evennia/web/templates/webclient/base.html @@ -113,6 +113,8 @@ JQuery available. + +