MediaWiki:IsTyping.js

/* IsTyping * * Displays which users are typing on chat. * Affects private messages by default. * * @scope site-wide * @author Dorumin */

(function { if ( mw.config.get('wgCanonicalSpecialPageName') !== 'Chat' || (window.IsTyping && IsTyping.init) ) {     return;  }

window.IsTyping = $.extend({   //     // Kill script default styling    noStyle: false,    // Kill script auto scroll compensation, not necessary after updated indicator    doScroll: false,    // Kill script on main    mainRoomDisabled: false,    // Kill script on PMs    privateRoomDisabled: false,    // Filter self or not, which I documented for some reason    filterSelf: true,    // Indicator jQuery object, defined on init    $indicator: null,    // Users whose typing state is hidden from the typing indicator    ignore: [],    // Use old configuration    old: false,    //, further overrides are possible but not supported    // Milliseconds to wait before a typing state is invalidated    statusTTL: 8000,    // Milliseconds before sending another typing ping to a room    ownStatusThrottle: 6000,    // Pixels to scroll by if [this.doScroll] is set to true    scrollBy: 20,    // For keeping track of loaded resources _preload: 0, // Object containing room data mapped by room ID   data: {}, // Returns the currently active room getCurrentRoom: function { if (mainRoom.activeRoom == 'main' || mainRoom.activeRoom === null) { return mainRoom; }     return mainRoom.chats.privates[mainRoom.activeRoom]; },   // Gets the corresponding object in [this.data] for a given room getRoomState: function(room) { return this.data[room.roomId]; },   // Sends a typing state to a room sendTypingState: function(status, room) { room = room || this.getCurrentRoom; var state = this.getRoomState(room), main = room.isMain; if (       (main  && this.mainRoomDisabled) ||        (!main && this.privateRoomDisabled)      ) return;

state.last = status ? Date.now : 0; room.socket.send(new models.SetStatusCommand({ statusMessage: 'typingState', statusState: status }).xport); },   // Adds someone to the typing list of a room startTyping: function(name, state, room) { if (state.typing.indexOf(name) == -1) { state.typing.push(name); }

if (state.timeouts[name]) { clearTimeout(state.timeouts[name]); }

state.timeouts[name] = setTimeout(this.stopTyping.bind(this, name, state, room), this.statusTTL); },   // Removes someone from the typing list of a room stopTyping: function(name, state, room) { var index = state.typing.indexOf(name);

if (index != -1) { state.typing.splice(index, 1); }     if (state.timeouts[name]) { clearTimeout(state.timeouts[name]); delete state.timeouts[name]; }

if (this.getCurrentRoom == room) { this.updateTypingIndicator(this.filterNames(state.typing)); }   },    // Filters self and ignored users before passing into updateTypingIndicator filterNames: function(names) { return names.filter(function(name) {       return (name != wgUserName || !this.filterSelf) && this.ignore.indexOf(name) == -1;      }.bind(this)); },   // Updates the state of a room from a socket event and the indicator if it's currently active updateTyping: function(room, name, status) { var state = this.getRoomState(room);

if (status) { this.startTyping(name, state, room); } else { this.stopTyping(name, state); }

if (this.getCurrentRoom == room) { this.updateTypingIndicator(this.filterNames(state.typing)); }   },    // Sets the text of the indicator element and does other DOM stuff, might have been able to split this one up some more updateTypingIndicator: function(names) { var $body = $(document.body), div = this.doScroll ? this.getCurrentRoom.viewDiscussion.chatDiv.get(0) : null;

if (!names.length) { $body.removeClass('is-typing'); IsTyping.$indicator.html(''); if (this.doScroll) { div.scrollHeight; /* trigger reflow */ div.scrollTop -= this.scrollBy; }       return; }

var tags = names.map(function(name) {       // .parse does the escaping        return ' ' + name + ' ';      }), message = names.length > 3 ? 'typing-more' : 'typing-' + names.length, args = [message].concat(tags);

$body.addClass('is-typing'); this.$indicator.html(this.i18n.msg.apply(window, args).parse); if (this.doScroll) { div.scrollHeight; /* trigger reflow */ div.scrollTop += this.scrollBy; }   },    // Called on socket updateUser messages, bound to a specific room socketHandler: function(room, message) { var type = room.isMain ? 'main' : 'private', data = JSON.parse(message.data).attrs, status = data.statusState, name = data.name;

if (data.statusMessage == 'typingState') { this.updateTyping(room, name, status); }   },    // Called for each new chat the user joins initChat: function(room) { this.data[room.roomId] = { timeouts: {}, // Object mapped by username of timeout IDs typing: [], // Usernames of the people typing, including main user last: 0    // Last time when a typing status was sent };     room.socket.on('updateUser', this.socketHandler.bind(this, room)); },   // Called when a new private room is opened, the roomId will always be accurate even with group PMs onPrivateRoom: function(user) { var roomId = user.attributes.roomId, room = mainRoom.chats.privates[roomId]; this.initChat(room); },   // Bound to each keydown event on the message textarea // It's a keydown event so it can detect backspace events and compare previous and next values with an immediate timeout onKeyDown: function(e) { var textarea = e.target, value = textarea.value, state = this.getRoomState(this.getCurrentRoom);

setTimeout(function {       if (value == textarea.value) return;        if (!textarea.value) {          this.sendTypingState(false);        } else if (Date.now - state.last > this.ownStatusThrottle) {          this.sendTypingState(true);        }      }.bind(this), 0); },   // Called when the message textarea loses focus onBlur: function { this.sendTypingState(false); },   // Called when a private message on the userlist is clicked before rooms are switched onPrivateClick: function { if (mainRoom.activeRoom == 'main' || !mainRoom.activeRoom) { this.sendTypingState(false, mainRoom); }   },    // Called after each lib is loaded preload: function { if (++this._preload == 2) { dev.i18n.loadMessages('IsTyping').then(this.init.bind(this)); }   },    // Initialization and double run check property, so don't try to override this one init: function(i18n) { this.i18n = i18n; i18n.useUserLang;

this.$indicator = this.$indicator || $(' ', {       class: 'typing-indicator'      }).appendTo('body');

this.initChat(mainRoom); mainRoom.model.privateUsers.bind('add', this.onPrivateRoom.bind(this)); mainRoom.viewDiscussion.getTextInput.on('keydown', this.onKeyDown.bind(this)); // Not added through the usual .bind because we need this to be called before the room is changed mainRoom.viewUsers._callbacks.privateListClick.unshift(this.onPrivateClick.bind(this)); if (this.old) { this.setupOldConfiguration; }   },    // For people who like to have their chat jiggle around setupOldConfiguration: function { this.$indicator.remove; this.$indicator = $(' ', {       class: 'typing-indicator'      }).prependTo('#Write'); this.doScroll = true; } }, window.IsTyping);

mw.hook('dev.i18n').add(IsTyping.preload.bind(IsTyping)); mw.hook('dev.chat.render').add(IsTyping.preload.bind(IsTyping));

if (IsTyping.old) { // Because Rappy doesn't like separate stylesheets mw.util.addCSS('body.is-typing .Chat {\     margin-bottom: 20px;\    }\    body.is-typing .typing-indicator {\      display: block;\    }\    .typing-indicator {\      display: none;\      left: 5px;\      line-height: 25px;\      position: absolute;\      right: 5px;\      top: -25px;\    }\    .typing-indicator .username {\      font-weight: bold;\    }\    body.warn .typing-indicator {\      top: -45px;\    }'); } else { // some dynamic css for good measure mw.util.addCSS('.typing-indicator {\     color: ' + getComputedStyle(document.getElementById('Write')).color + '\    }'); importArticle({     type: 'stylesheet',      article: 'u:dev:MediaWiki:IsTyping/code.css'    }); } importArticles({    type: 'script',    articles: [      'u:dev:MediaWiki:I18n-js/code.js',      'u:dev:MediaWiki:Chat-js.js'    ]  }); });