BattleSim/client/app/js/views/battles/chat_view.coffee

245 lines
7.9 KiB
CoffeeScript

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("<div class='alert alert-info'><b>Topic:</b> #{topic}</div>")
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 = "<b class='open_pm fake_link' data-user-id='#{username}'
style='color: #{@userColor(username)}'>#{displayName}:</b>"
@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("<p class='chat_message #{klass.join(' ')}'>#{message}</p>")
@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("<div class='alert alert-#{klass} clearfix'>#{message}</div>")
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)
"<span class='monospace'>[#{hours}:#{minutes}:#{seconds}]</span>"
# Escapes all HTML, but also converts links to clickable links.
sanitize: (message) =>
sanitizedMessage = $('<div/>').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 = $("<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("<div/>").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