class @ChatView extends Backbone.View template: JST['chat'] userListTemplate: JST['user_list'] events: 'click': 'focusChat' 'keydown .chat_input': 'handleKeys' 'click .chat_input_send': 'sendChat' 'scroll_to_bottom': 'scrollToBottom' MAX_USERNAME_HISTORY = 10 MAX_MESSAGES_LENGTH = 500 # Takes a `model`, which is a Room instance. initialize: (options) => {@noisy} = options if @model @listenTo(@model.get('users'), 'add remove reset', @renderUserList) if @noisy @listenTo(@model.get('users'), 'add', @userJoin) @listenTo(@model.get('users'), 'remove', @userLeave) for eventName in Room::EVENTS callback = this[eventName] || throw new Error("ChatView must implement #{eventName}.") @listenTo(@model, eventName, callback) @chatHistory = [] @mostRecentNames = [] @tabCompleteIndex = -1 @tabCompleteNames = [] # Sets the channel topic setTopic: (topic) => topic = @sanitize(topic) @rawMessage("
Topic: #{topic}
") render: => @$el.html @template() if @model @$el.removeClass('without_spectators') @$el.removeClass('without_chat_input') @renderUserList() this renderUserList: => @$('.user_count').text "Users (#{@model.get('users').length})" @$('.users').html @userListTemplate(userList: @model.get('users').models) this getSelectedText: => text = "" if window.getSelection text = window.getSelection().toString() else if document.selection && document.selection.type != "Control" text = document.selection.createRange().text return text focusChat: => selectedText = @getSelectedText() @$('.chat_input').focus() if selectedText.length == 0 sendChat: => $this = @$('.chat_input') message = $this.val() if @model.sendChat(message) @chatHistory.push(message) delete @chatHistoryIndex $this.val('') tabComplete: ($input, options = {}) => cursorIndex = $input.prop('selectionStart') text = $input.val() if @tabCompleteNames.length > 0 && @tabCompleteCursorIndex == cursorIndex if options.reverse @tabCompleteIndex -= 1 if @tabCompleteIndex < 0 @tabCompleteIndex = @tabCompleteNames.length - 1 else @tabCompleteIndex = (@tabCompleteIndex + 1) % @tabCompleteNames.length else delete @tabCompleteCursorIndex pieces = text[0...cursorIndex].split(' ') possibleName = pieces.pop() rest = pieces.join(' ') rest += ' ' if pieces.length > 0 # Append a space if a word exists length = possibleName.length return if length == 0 candidates = _.union(@mostRecentNames, @model.get('users').pluck('id')) candidates = candidates.filter (name) -> name[...length].toLowerCase() == possibleName.toLowerCase() return if candidates.length == 0 if options.reverse @tabCompleteIndex = candidates.length - 1 else @tabCompleteIndex = 0 @tabCompleteNames = candidates @tabCompletePrefix = rest @tabCompleteCursorIndex = cursorIndex tabbedName = @tabCompleteNames[@tabCompleteIndex] newPrefix = @tabCompletePrefix + tabbedName newPrefixLength = newPrefix.length $input.val(newPrefix + text[cursorIndex...]) $input[0].setSelectionRange(newPrefixLength, newPrefixLength) @tabCompleteCursorIndex = newPrefixLength handleKeys: (e) => $input = $(e.currentTarget) switch e.which when 13 # [Enter] e.preventDefault() @sendChat() when 9 # [Tab] e.preventDefault() @tabComplete($input, reverse: e.shiftKey) when 38 # [Up arrow] e.preventDefault() return if @chatHistory.length == 0 if !@chatHistoryIndex? @chatHistoryIndex = @chatHistory.length @chatHistoryText = $input.val() if @chatHistoryIndex > 0 @chatHistoryIndex -= 1 $input.val(@chatHistory[@chatHistoryIndex]) when 40 # [Down arrow] e.preventDefault() return unless @chatHistoryIndex? @chatHistoryIndex += 1 if @chatHistoryIndex == @chatHistory.length $input.val(@chatHistoryText) delete @chatHistoryIndex else $input.val(@chatHistory[@chatHistoryIndex]) userMessage: (username, message) => user = @model.get('users').get(username) displayName = user?.getDisplayName() || username yourName = PokeBattle.username highlight = (new RegExp("\\b#{yourName}\\b", 'i').test(message)) # Render the chat message u = "#{displayName}:" @rawMessage("#{@timestamp()} #{u} #{@sanitize(message)}", {highlight}) # We might want to run something based on the message, e.g. !pbv from a mod. @handleMessage(user, message) # Record last few usernames who chatted index = @mostRecentNames.indexOf(username) @mostRecentNames.splice(index, 1) if index != -1 @mostRecentNames.push(username) @mostRecentNames.shift() if @mostRecentNames.length > MAX_USERNAME_HISTORY userColor: (username) => # Same hashing algorithm as in Java hash = 0 for c, i in username chr = username.charCodeAt(i) hash = ((hash << 5) - hash) + chr hash |= 0 h = hash % 360 hash /= 360 s = (hash % 25) + 75 l = 50 "hsl(#{h}, #{s}%, #{l}%)" handleMessage: (user, message) => authority = user?.get('authority') printableCommands = ['/pbv', '/data'] # TODO: no magic constants. '1' is a regular user. if authority > 1 && message.split(/\s/, 1)[0] in printableCommands PokeBattle.commands.execute(@model, message) userJoin: (user) => @rawMessage("#{@timestamp()} #{user.id} joined!") userLeave: (user) => @rawMessage("#{@timestamp()} #{user.id} left!") rawMessage: (message, options = {}) => wasAtBottom = @isAtBottom() klass = [] klass.push('bg-blue') if options.highlight klass.push(options.class) if options.class @print("

#{message}

") @cleanChat() if wasAtBottom then @scrollToBottom() cleanChat: => $messages = @$('.chat_message') numToRemove = ($messages.length - MAX_MESSAGES_LENGTH) if numToRemove > 0 $messages.slice(0, numToRemove).remove() announce: (klass, message) => wasAtBottom = @isAtBottom() message = @linkify(message) @print("
#{message}
") if wasAtBottom then @scrollToBottom() print: (message) => @$('.messages').append(message) clear: => @$('.messages').empty() timestamp: => date = new Date() hours = date.getHours() minutes = date.getMinutes() seconds = date.getSeconds() minutes = "00#{minutes}".substr(-2) seconds = "00#{seconds}".substr(-2) "[#{hours}:#{minutes}:#{seconds}]" # Escapes all HTML, but also converts links to clickable links. sanitize: (message) => sanitizedMessage = $('
').text(message).html() @linkify(sanitizedMessage) linkify: (message) => message = URI.withinString message, (url) -> uri = URI(url) [host, path] = [uri.host(), uri.path()] battleRegex = /^\/battles\/([a-fA-F0-9]+)$/i $a = $("").prop('href', url).prop('target', '_blank').text(url) if host == URI(window.location.href).host() && battleRegex.test(path) battleId = path.match(battleRegex)[1] $a.addClass('spectate').attr('data-battle-id', battleId) return $a.wrap("
").parent().html() message # Returns true if the chat is scrolled to the bottom of the screen. # This also returns true if the messages are hidden. isAtBottom: => $el = @$('.messages') ($el[0].scrollHeight - $el.scrollTop() <= $el.outerHeight()) scrollToBottom: => messages = @$('.messages')[0] messages.scrollTop = messages.scrollHeight false