10
\$\begingroup\$

In SE chat, there are two different ways to mention people: A "ping" to just get a user's attention, and a "reply" that points out a specific message by the user. This script makes it so that beginning a message with (by default) @username@, the message is made a reply to username's most recent message. Additionally, any message that begins with @username* is never sent, but username's last message is starred.

I have very little experience with Javascript, and even less with jQuery. I wrote this code flying by the seat of my pants. I googled a lot of things, referenced a lot of things used in this script, and guessed at a lot of others. It works, sure, but that last sentence doesn't inspire a lot of confidence. I don't really have anything specifically I want to ask about, since I don't know what stuff it is that I don't know! :P That being said, I'm sure there are probably a lot of problems with it that someone more experienced than I would be able to identify.

// ==UserScript==
// @name easyreply
// @namespace http://reddit.com/u/undergroundmonorail
// @author monorail
// @version 0.2
// @description easily respond to a user's last message in se chat
// @grant none
// @copyright MIT
// @include *://chat.stackoverflow.com/*
// @include *://chat.stackexchange.com/*
// @include *://chat.meta.stackexchange.com/*
// ==/UserScript==

// workaround for Safari
// (as if anyone uses Safari)
// ((glares at @AlexA.))
function withJQuery(f) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.textContent = '('+ f + ')(jQuery)';
    document.head.appendChild(script);
}

withJQuery(easyreply);

function easyreply($) {
    // OPTIONS
    // prefix/suffix of username to reply to
    var R_PREFIX = '@';
    var R_SUFFIX = '@';
    // prefix/suffix of username to star
    var S_PREFIX = '@';
    var S_SUFFIX = '*';

    var input = $('#input');

    function messageAuthor(message) {
        // a group of continuous messages by the same user is a "monologue"
        // go up to the monologue, then find the author
        // strip spaces from the username because that's how the name you type to ping someone is found
        // (e.g.) pinging 'Alex A.' is done by typing '@AlexA.')
        return message.parent().parent().children('.signature').children('.username').text().replace(/ /g, '');
    }

    function lastMessageFrom(author) {
            // go backwards through the messages until finding one that is by the mentioned user OR there are no more messages
            var message = $('.message:last');
            while (message.length && author.toLowerCase() !== messageAuthor(message).toLowerCase()) {
                var prev = message.prev('.message'); // go up one message
                if (!prev.length) { // handle monologue barriers
                    // stole this code wholesale from doorknob. i mostly understand it.
                    prev = message.closest('.monologue').prevAll('.monologue').eq(0).find('.message:last');
                }
                message = prev;
            }
            return message; 
    }

    function replyToMessage(message) {
        if (message.length) { // if a message was found
            // reply syntax is ':[message id]'
            // messages have the attribute 'id', containing 'message-[message id]'
            // put a colon, everything after the hyphen in the id, then the original message minus the first word
            input[0].value = ':' + message.attr('id').split('-')[1] + ' ' + input[0].value.split(' ').slice(1).join(' ');
        }
    }
1   
    function starMessage(message) {
        message.find('.stars:first .vote').click();
    }

    input.keydown(function(e) {
        if (e.which === 13) { // when enter key is pressed
            e.preventDefault();

            var firstWord = input[0].value.split(' ')[0];
            var R_author = firstWord.slice(R_PREFIX.length, -R_SUFFIX.length); // author name if reply
            var S_author = firstWord.slice(S_PREFIX.length, -R_SUFFIX.length); // author name if star

            if (R_PREFIX + R_author + R_SUFFIX === firstWord) {
                replyToMessage(lastMessageFrom(R_author));
            } else if (S_PREFIX + S_author + S_SUFFIX === firstWord) {
                starMessage(lastMessageFrom(S_author));
                input[0].value = ''; // no message is sent, clear input box manually
            }
        }
    });

    // i don't know what this means but doorknob's code has it ¯\_(ツ)_/¯
    var kdevts = $('#input').data('events').keydown;
    kdevts.unshift(kdevts.pop());
    // doorknob: "For the record, it moves the last keydown event to the front of the 'list' of events."
    // "That way your function is called before SE's default functions."
    // "Otherwise, the message would be sent *before* the replacement would be made."
    // thanks doorknob
}
\$\endgroup\$
  • \$\begingroup\$ could you explain me why you need that workaround for Safari? What issue is it trying to solve? \$\endgroup\$ – Zorgatone Jan 10 '16 at 13:52
  • 1
    \$\begingroup\$ @Zorgatone My understanding is that Safari doesn't let you access the webpage's jQuery from a userscript, but will let you access it from a script element on the page. To be 100% honest I haven't tested this script in Safari at all (I don't even think it's available for Linux and I haven't had easy access to Windows since I wrote it) but this code is based on a userscript that does have that and is written by someone I trust to know what they're doing. :P \$\endgroup\$ – undergroundmonorail Jan 10 '16 at 16:58
  • \$\begingroup\$ Well ok. Good to know \$\endgroup\$ – Zorgatone Jan 10 '16 at 18:08
  • \$\begingroup\$ That kinda sucks though. I don't like exposing a global function for a purpose like this, when you shouldn't.. :( I always have an IIFE or any kind of closure to keep stuff clean and safe \$\endgroup\$ – Zorgatone Jan 10 '16 at 18:13
1
\$\begingroup\$

Your code is quite nice, so I don't have that much to say about it. Here are a few points I noticed about it.


Store the used array element.

At the beginning of your code, you do this:

var input = $('#input');

Now, everywhere else when you are using this input variable, you are doing this:

...input[0]...

Wouldn't it be a lot more efficient to just store this value in the first place?

var input = $('#input')[0];

Now you don't have to keep on constantly accessing this array index.


Simplify with regex

After creating some sample chat messages and looking over them, this is the format that special beginning part of the chat message:

/^@(.+)(@|*)/

In case, you aren't familiar with regex:

  • ^@: The first character must be a @.
  • (.+): This grabs the username portion
  • (@|*): After the username, there must either a @ or *.

Now, let's use this chat message as an example:

"@SirPython@ You are so cool!"

Using this regex and .split on this string will yield this array:

[
    "",
    "SirPython",
    "@",
    " You are so cool!"
];

Note: The "@" of the array is that last @, not the first one. Therefore, it could be a *

See how this could be useful now? In the keydown listener function, you can use this regex to cleanly parse the chat message and handle the different parts:

// If this chat message doesn't use the special part, stop right away
if(!regex.test(string)) {
    return;
}

var parts = input.value.split(regex);
var author = parts[1];
var isReply = parts[2] === "@";
var message = parts[3];

if(isReply) {
    replyToMessage(lastMessageFrom(author));
} else {
    starMessage(lastMessageFrom(author));
    input.value = "";
}

See? This code is much cleaner now that it doesn't have to do those weird string slicing operations and what-not. Also, it doesn't need those specific R_PREFIX and S_PREFIX variables, nor does it need the R_author or S_author variables (which, by the way, were the same exact thing).

Also, it is more efficient because it now has an immediate return if the chat message doesn't meet the format.

The regex, which should be placed near the top of the easyreply function so it has time to load, shows cleanly the desired format of the chat message. If you want, since the return from .split(regex) can be sort-of weird, you can create some constants to label the indexes to make your code even cleaner.

For example:

parts[AUTHOR_INDEX];

to get the author.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.