1
0
mirror of https://github.com/wbbaddons/Tims-Chat.git synced 2024-10-31 14:10:08 +00:00
Tims-Chat/file/js/be.bastelstu.Chat.litcoffee

658 lines
19 KiB
Plaintext
Raw Normal View History

2013-05-15 19:55:51 +00:00
Tims Chat 3
===========
2013-05-15 19:55:51 +00:00
This is the main javascript file for [**Tims Chat**](https://github.com/wbbaddons/Tims-Chat). It handles
everything that happens in the GUI of **Tims Chat**.
### Copyright Information
2013-05-14 18:29:44 +00:00
# @author Tim Düsterhus
# @copyright 2010-2013 Tim Düsterhus
# @license Creative Commons Attribution-NonCommercial-ShareAlike <http://creativecommons.org/licenses/by-nc-sa/3.0/legalcode>
2013-05-15 19:55:51 +00:00
# @package be.bastelstu.chat
2013-05-14 18:29:44 +00:00
###
2013-05-15 19:55:51 +00:00
## Code
We start by setting up our environment by ensuring some sane values for both `$` and `window`,
2013-05-15 20:11:47 +00:00
enabling EMCAScript 5 strict mode and overwriting console to prepend the name of the class.
(($, window) ->
"use strict";
console =
log: (message) ->
window.console.log "[be.bastelstu.Chat] #{message}"
warn: (message) ->
window.console.warn "[be.bastelstu.Chat] #{message}"
error: (message) ->
window.console.error "[be.bastelstu.Chat] #{message}"
2013-05-15 20:11:47 +00:00
Continue with defining the needed variables. All variables are local to our closure and will be
2013-05-15 19:55:51 +00:00
exposed by a function if necessary.
2013-05-15 19:55:51 +00:00
isActive = true
newMessageCount = 0
2013-05-24 13:30:18 +00:00
chatSession = Date.now()
2013-05-24 13:55:52 +00:00
errorVisible = false
2013-05-15 19:55:51 +00:00
remainingFailures = 3
2013-05-15 19:55:51 +00:00
events =
newMessage: $.Callbacks()
userMenu: $.Callbacks()
submit: $.Callbacks()
2013-05-15 19:55:51 +00:00
pe =
getMessages: null
refreshRoomList: null
fish: null
2013-05-15 19:55:51 +00:00
loading = false
2013-05-15 19:55:51 +00:00
autocomplete =
2013-05-10 15:51:48 +00:00
offset: 0
value: null
caret: 0
2013-05-15 19:55:51 +00:00
v =
titleTemplate: null
messageTemplate: null
userTemplate: null
2013-05-15 19:55:51 +00:00
config: null
2013-05-15 19:55:51 +00:00
Initialize **Tims Chat**. Bind needed DOM events and initialize data structures.
2013-05-15 19:55:51 +00:00
initialized = false
init = (config, titleTemplate, messageTemplate, userTemplate) ->
return false if initialized
initialized = true
2013-05-15 19:55:51 +00:00
v.config = config
v.titleTemplate = titleTemplate
v.messageTemplate = messageTemplate
v.userTemplate = userTemplate
console.log 'Initializing'
When **Tims Chat** becomes focused mark the chat as active and remove the number of new messages from the title.
2013-05-15 19:55:51 +00:00
$(window).focus ->
document.title = v.titleTemplate.fetch
title: $('#timsChatRoomList .active a').text()
2013-05-15 19:55:51 +00:00
newMessageCount = 0
isActive = true
2013-05-15 19:55:51 +00:00
When **Tims Chat** loses the focus mark the chat as inactive.
2013-05-15 19:55:51 +00:00
$(window).blur ->
isActive = false
2013-05-15 19:55:51 +00:00
Make the user leave the chat when **Tims Chat** is about to be unloaded.
2013-05-15 19:55:51 +00:00
$(window).on 'beforeunload', ->
2013-05-24 17:26:29 +00:00
return undefined if errorVisible
2013-05-15 19:55:51 +00:00
new WCF.Action.Proxy
autoSend: true
data:
actionName: 'leave'
className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false
async: false
suppressErrors: true
undefined
2013-05-15 19:55:51 +00:00
Insert the appropriate smiley code into the input when a smiley is clicked.
2013-05-15 19:55:51 +00:00
$('#smilies').on 'click', 'img', ->
insertText ' ' + $(@).attr('alt') + ' '
Handle submitting the form. The message will be validated by some basic checks, passed to the `submit` eventlisteners
and afterwards sent to the server by an AJAX request.
2013-05-15 19:55:51 +00:00
$('#timsChatForm').submit (event) ->
event.preventDefault()
2013-05-15 19:55:51 +00:00
text = $('#timsChatInput').val().trim()
$('#timsChatInput').val('').focus().keyup()
2013-05-15 19:55:51 +00:00
return false if text.length is 0
2013-05-15 19:55:51 +00:00
# Free the fish!
freeTheFish() if text.toLowerCase() is '/free the fish'
2013-05-15 19:55:51 +00:00
text = do (text) ->
obj =
text: text
events.submit.fire obj
obj.text
2013-05-15 19:55:51 +00:00
new WCF.Action.Proxy
autoSend: true
data:
actionName: 'send'
className: 'chat\\data\\message\\MessageAction'
parameters:
text: text
enableSmilies: $('#timsChatSmilies').data 'status'
showLoadingOverlay: false
success: ->
$('#timsChatInputContainer').removeClass('formError').find('.innerError').hide()
getMessages()
failure: (data) ->
return true unless (data?.returnValues?.errorType?) or (data?.message?)
$('#timsChatInputContainer').addClass('formError').find('.innerError').show().html (data?.returnValues?.errorType) ? data.message
setTimeout ->
$('#timsChatInputContainer').removeClass('formError').find('.innerError').hide()
, 5e3
2013-05-18 19:42:27 +00:00
false
2013-05-15 19:55:51 +00:00
Autocomplete a username when TAB is pressed. The name to autocomplete is based on the current caret position.
The the word the caret is in will be passed to `autocomplete` and replaced if a match was found.
2013-05-15 19:55:51 +00:00
$('#timsChatInput').keydown (event) ->
if event.keyCode is $.ui.keyCode.TAB
input = $(event.currentTarget)
event.preventDefault()
2013-05-15 19:55:51 +00:00
autocomplete.value ?= input.val()
autocomplete.caret ?= input.getCaret()
beforeCaret = autocomplete.value.substring 0, autocomplete.caret
lastSpace = beforeCaret.lastIndexOf ' '
beforeComplete = autocomplete.value.substring 0, lastSpace + 1
toComplete = autocomplete.value.substring lastSpace + 1
nextSpace = toComplete.indexOf ' '
if nextSpace is -1
afterComplete = '';
else
afterComplete = toComplete.substring nextSpace + 1
toComplete = toComplete.substring 0, nextSpace
return if toComplete.length is 0
console.log "Autocompleting '#{toComplete}'"
regex = new RegExp "^#{WCF.String.escapeRegExp(toComplete)}", "i"
users = (username for user in $('.timsChatUser') when regex.test(username = $(user).data('username')))
2013-05-15 19:55:51 +00:00
toComplete = users[autocomplete.offset++ % users.length] + ', ' if users.length isnt 0
input.val "#{beforeComplete}#{toComplete}#{afterComplete}"
input.setCaret (beforeComplete + toComplete).length
2013-05-15 19:55:51 +00:00
Reset autocompleter to default status, when a key is pressed that is not TAB.
2013-05-15 19:55:51 +00:00
else
$('#timsChatInput').click()
2013-05-15 19:55:51 +00:00
Reset autocompleter to default status, when the input is `click`ed, as the position of the caret may have changed.
2013-05-15 19:55:51 +00:00
$('#timsChatInput').click ->
autocomplete =
offset: 0
value: null
caret: null
2013-05-15 19:55:51 +00:00
Refresh the room list when the associated button is `click`ed.
2013-05-15 19:55:51 +00:00
$('#timsChatRoomList button').click ->
2013-05-17 18:23:59 +00:00
refreshRoomList()
2013-04-27 11:04:36 +00:00
2013-05-15 19:55:51 +00:00
Clear the chat by removing every single message once the clear button is `clicked`.
2013-05-15 19:55:51 +00:00
$('#timsChatClear').click (event) ->
event.preventDefault()
$('.timsChatMessage').remove()
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer').prop('scrollHeight')
2013-05-15 19:55:51 +00:00
Handle toggling of the toggleable buttons.
$('.timsChatToggle').click (event) ->
element = $ @
if element.data('status') is 1
element.data 'status', 0
element.removeClass 'active'
element.attr 'title', element.data 'enableMessage'
else
element.data 'status', 1
element.addClass 'active'
element.attr 'title', element.data 'disableMessage'
$('#timsChatInput').focus()
2013-04-22 16:52:05 +00:00
2013-05-15 19:55:51 +00:00
Mark smilies as disabled when they are disabled.
2013-04-22 16:52:05 +00:00
2013-05-15 19:55:51 +00:00
$('#timsChatSmilies').click (event) ->
if $(@).data 'status'
$('#smilies').removeClass 'disabled'
else
$('#smilies').addClass 'disabled'
2013-04-22 16:52:05 +00:00
Toggle fullscreen mode.
2013-05-15 19:55:51 +00:00
$('#timsChatFullscreen').click (event) ->
if $('#timsChatFullscreen').data 'status'
$('html').addClass 'fullscreen'
else
$('html').removeClass 'fullscreen'
2013-04-26 21:00:48 +00:00
Toggle checkboxes
2013-05-15 19:55:51 +00:00
$('#timsChatMark').click (event) ->
if $(@).data 'status'
$('.timsChatMessageContainer').addClass 'markEnabled'
else
$('.timsChatMessageContainer').removeClass 'markEnabled'
2013-04-26 21:00:48 +00:00
2013-05-15 19:55:51 +00:00
Visibly mark the message once the associated checkbox is checked.
$(document).on 'click', '.timsChatMessage :checkbox', (event) ->
if $(@).is ':checked'
$(@).parents('.timsChatMessage').addClass('jsMarked')
else
$(@).parents('.timsChatMessage').removeClass('jsMarked')
2013-04-22 16:52:05 +00:00
Scroll down when autoscroll is being activated.
2013-05-15 19:55:51 +00:00
$('#timsChatAutoscroll').click (event) ->
if $('#timsChatAutoscroll').data 'status'
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer').prop('scrollHeight')
2013-05-15 19:55:51 +00:00
$('#timsChatMessageContainer').on 'scroll', (event) ->
element = $ @
scrollTop = element.scrollTop()
scrollHeight = element.prop 'scrollHeight'
height = element.height()
if scrollTop < scrollHeight - height - 25
2013-05-15 19:55:51 +00:00
if $('#timsChatAutoscroll').data('status') is 1
$('#timsChatAutoscroll').click()
if scrollTop > scrollHeight - height - 10
if $('#timsChatAutoscroll').data('status') is 0
$('#timsChatAutoscroll').click()
2013-05-24 13:30:18 +00:00
Enable duplicate tab detection.
window.localStorage.setItem 'be.bastelstu.chat.session', chatSession
$(window).on 'storage', (event) ->
if event.originalEvent.key is 'be.bastelstu.chat.session'
if event.originalEvent.newValue isnt chatSession
2013-05-24 17:26:29 +00:00
showError WCF.Language.get 'chat.error.duplicateTab'
2013-05-24 13:30:18 +00:00
Ask for permissions to use Desktop notifications when notifications are activated.
2013-05-15 19:55:51 +00:00
if window.Notification?
$('#timsChatNotify').click (event) ->
return unless $(@).data 'status'
if window.Notification.permission isnt 'granted'
window.Notification.requestPermission (permission) ->
window.Notification.permission ?= permission
2013-05-15 19:55:51 +00:00
events.newMessage.add notify
Initialize the `PeriodicalExecuter`s and run them once.
2013-05-15 19:55:51 +00:00
pe.refreshRoomList = new WCF.PeriodicalExecuter refreshRoomList, 60e3
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
refreshRoomList()
getMessages()
2013-05-15 19:55:51 +00:00
Initialize the [**nodePush**](https://github.com/wbbaddons/nodePush) integration of **Tims Chat**. Once
the browser is connected to **nodePush** periodic message loading will be disabled and **Tims Chat** will
load messages if the appropriate event arrives.
do ->
be.bastelstu.wcf.nodePush.onConnect ->
console.log 'Disabling periodic loading'
pe.getMessages.stop()
2013-05-15 19:55:51 +00:00
be.bastelstu.wcf.nodePush.onDisconnect ->
console.log 'Enabling periodic loading'
getMessages()
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
2013-05-15 19:55:51 +00:00
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.chat.newMessage', getMessages
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.wcf.nodePush.tick60', getMessages
2013-05-15 19:55:51 +00:00
Finished! Enable the input now.
2013-05-15 19:55:51 +00:00
$('#timsChatInput').enable().jCounter().focus();
2013-05-15 19:55:51 +00:00
console.log "Finished initializing"
2013-05-15 19:55:51 +00:00
true
2013-05-15 19:55:51 +00:00
Free the fish.
2013-05-15 19:55:51 +00:00
freeTheFish = ->
return if $.wcfIsset 'fish'
console.warn 'Freeing the fish'
fish = $ """<div id="fish">#{WCF.String.escapeHTML('><((((\u00B0>')}</div>"""
fish.css
position: 'absolute'
top: '150px'
left: '400px'
color: 'black'
textShadow: '1px 1px white'
zIndex: 9999
fish.appendTo $ 'body'
pe.fish = new WCF.PeriodicalExecuter ->
left = Math.random() * 100 - 50
top = Math.random() * 100 - 50
fish = $ '#fish'
2013-05-15 19:55:51 +00:00
left *= -1 unless fish.width() < (fish.position().left + left) < ($(document).width() - fish.width())
top *= -1 unless fish.height() < (fish.position().top + top) < ($(document).height() - fish.height())
if left > 0
fish.text '><((((\u00B0>' if left > 0
2013-05-15 19:55:51 +00:00
else if left < 0
fish.text '<\u00B0))))><'
fish.animate
top: "+=#{top}"
left: "+=#{left}"
, 1e3
, 1.5e3
Fetch new messages from the server and pass them to `handleMessages`. The userlist will be passed to `handleUsers`.
`remainingFailures` will be decreased on failure and message loading will be entirely disabled once it reaches zero.
getMessages = ->
$.ajax v.config.messageURL,
dataType: 'json'
type: 'POST'
success: (data) ->
remainingFailures = 3
WCF.DOMNodeInsertedHandler.enable()
handleMessages data.messages
handleUsers data.users
WCF.DOMNodeInsertedHandler.disable()
error: ->
console.error "Message loading failed, #{--remainingFailures} remaining"
if remainingFailures <= 0
freeTheFish()
2013-05-24 13:55:52 +00:00
console.error 'To many failures, aborting'
2013-05-15 19:55:51 +00:00
2013-05-24 17:26:29 +00:00
showError WCF.Language.get 'chat.error.onMessageLoad'
2013-04-26 21:10:33 +00:00
2013-05-15 19:55:51 +00:00
complete: ->
loading = false
2013-05-15 19:55:51 +00:00
Prevent loading messages in parallel.
2013-05-15 19:55:51 +00:00
beforeSend: ->
return false if loading
loading = true
2013-05-15 19:55:51 +00:00
Insert the given messages into the chat stream.
2013-05-15 19:55:51 +00:00
handleMessages = (messages) ->
$('#timsChatMessageContainer').trigger 'scroll'
2013-05-15 19:55:51 +00:00
for message in messages
events.newMessage.fire message
2013-05-15 19:55:51 +00:00
output = v.messageTemplate.fetch message
li = $ '<li></li>'
li.addClass 'timsChatMessage'
li.addClass "timsChatMessage#{message.type}"
li.addClass "user#{message.sender}"
li.addClass 'ownMessage' if message.sender is WCF.User.userID
li.append output
li.appendTo $ '#timsChatMessageContainer > ul'
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer').prop('scrollHeight') if $('#timsChatAutoscroll').data('status') is 1
2013-05-15 19:55:51 +00:00
Rebuild the userlist based on the given `users`.
2013-05-15 19:55:51 +00:00
handleUsers = (users) ->
foundUsers = { }
2013-05-15 19:55:51 +00:00
for user in users
2013-05-15 20:11:47 +00:00
id = "timsChatUser#{user.userID}"
2013-05-15 19:55:51 +00:00
Move the user to the new position if he was found in the old list.
2013-05-15 20:11:47 +00:00
if $.wcfIsset id
2013-05-15 19:55:51 +00:00
console.log "Moving User: '#{user.username}'"
2013-05-15 20:11:47 +00:00
element = $("##{id}").detach()
2013-05-15 19:55:51 +00:00
if user.awayStatus?
element.addClass 'away'
element.attr 'title', user.awayStatus
else
element.removeClass 'timsChatAway'
element.removeAttr 'title'
element.data 'tooltip', ''
if user.suspended
element.addClass 'suspended'
else
element.removeClass 'suspended'
$('#timsChatUserList > ul').append element
2013-05-15 19:55:51 +00:00
Build HTML of the user and insert it into the list, if the users was not found in the chat before.
2013-05-15 19:55:51 +00:00
else
console.log "Inserting User: '#{user.username}'"
li = $ '<li></li>'
2013-05-15 19:55:51 +00:00
li.attr 'id', id
li.addClass 'timsChatUser'
li.addClass 'jsTooltip'
li.addClass 'dropdown'
li.addClass 'you' if user.userID is WCF.User.userID
li.addClass 'suspended' if user.suspended
if user.awayStatus?
li.addClass 'timsChatAway'
li.attr 'title', user.awayStatus
li.data 'username', user.username
2013-05-15 19:55:51 +00:00
li.append v.userTemplate.fetch user
2013-05-15 19:55:51 +00:00
menu = $ '<ul></ul>'
menu.addClass 'dropdownMenu'
menu.append $ "<li><a>#{WCF.Language.get('chat.general.query')}</a></li>"
menu.append $ "<li><a>#{WCF.Language.get('chat.general.kick')}</a></li>"
menu.append $ "<li><a>#{WCF.Language.get('chat.general.ban')}</a></li>"
menu.append $ """<li><a href="#{user.link}">#{WCF.Language.get('chat.general.profile')}</a></li>"""
2013-05-15 20:11:47 +00:00
2013-05-15 19:55:51 +00:00
events.userMenu.fire user, menu
2013-05-15 20:11:47 +00:00
li.append menu
2013-05-15 19:55:51 +00:00
li.appendTo $ '#timsChatUserList > ul'
foundUsers[id] = true
2013-05-15 19:55:51 +00:00
Remove all users that left the chat.
2013-05-15 19:55:51 +00:00
$('.timsChatUser').each ->
unless foundUsers[$(@).attr('id')]?
console.log "Removing User: '#{$(@).data('username')}'"
$(@).remove();
$('#toggleUsers .badge').text $('.timsChatUser').length
2013-05-15 19:55:51 +00:00
Insert the given `text` into the input. If `options.append` is true the given `text` will be appended, otherwise it will replaced
the existing text. If `options.submit` is true the message will be sent to the server afterwards.
2013-05-15 19:55:51 +00:00
insertText = (text, options = { }) ->
options = $.extend
append: true
submit: false
, options
text = $('#timsChatInput').val() + text if options.append
$('#timsChatInput').val text
$('#timsChatInput').keyup()
if (options.submit)
$('#timsChatForm').submit()
else
$('#timsChatInput').focus()
2013-05-15 19:55:51 +00:00
Send out notifications for the given `message`. The number of unread messages will be prepended to `document.title` and if available desktop notifications will be sent.
2013-05-15 19:55:51 +00:00
notify = (message) ->
return if isActive or $('#timsChatNotify').data('status') is 0
document.title = v.titleTemplate.fetch
title: $('#timsChatRoomList .active a').text()
newMessageCount: ++newMessageCount
title = WCF.Language.get 'chat.general.notify.title'
content = "#{message.username}#{message.separator} #{message.message}"
if window.Notification?.permission is 'granted'
do ->
notification = new window.Notification title,
body: content
onclick: ->
notification.close()
setTimeout ->
notification.close()
, 5e3
Fetch the roomlist from the server and update it in the GUI.
refreshRoomList = ->
console.log 'Refreshing the roomlist'
$('#toggleRooms .ajaxLoad').show()
new WCF.Action.Proxy
autoSend: true
data:
actionName: 'getRoomList'
className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false
suppressErrors: true
success: (data) ->
$('#timsChatRoomList li').remove()
$('#toggleRooms .ajaxLoad').hide()
$('#toggleRooms .badge').text data.returnValues.length
2013-05-15 19:55:51 +00:00
for room in data.returnValues
li = $ '<li></li>'
li.addClass 'active' if room.active
$("""<a href="#{room.link}">#{room.title}</a>""").addClass('timsChatRoom').appendTo li
$('#timsChatRoomList ul').append li
2013-05-15 19:55:51 +00:00
if window.history?.replaceState?
$('.timsChatRoom').click (event) ->
event.preventDefault()
target = $(@)
2013-05-15 19:55:51 +00:00
window.history.replaceState {}, '', target.attr 'href'
$.ajax target.attr('href'),
dataType: 'json'
data:
ajax: 1
type: 'POST'
success: (data, textStatus, jqXHR) ->
loading = false
target.parent().removeClass 'loading'
$('.active .timsChatRoom').parent().removeClass 'active'
target.parent().addClass 'active'
$('#timsChatTopic').text data.topic
if data.topic is ''
$('#timsChatTopic').addClass 'empty'
else
$('#timsChatTopic').removeClass 'empty'
$('.timsChatMessage').addClass 'unloaded'
handleMessages data.messages
document.title = v.titleTemplate.fetch data
2013-05-15 19:55:51 +00:00
Reload the whole page when an error occurs. The users thus sees the error message (usually `PermissionDeniedException`)
2013-05-15 19:55:51 +00:00
error: ->
window.location.reload true
2013-05-15 19:55:51 +00:00
Show loading icon and prevent switching the room in parallel.
2013-05-15 19:55:51 +00:00
beforeSend: ->
return false if target.parent().hasClass('loading') or target.parent().hasClass 'active'
loading = true
target.parent().addClass 'loading'
console.log "Found #{data.returnValues.length} rooms"
2013-05-24 13:55:52 +00:00
Shows an unrecoverable error with the given text.
showError = (text) ->
return if errorVisible
errorVisible = true
loading = true
pe.refreshRoomList.stop()
pe.getMessages.stop()
errorDialog = $("""
<div id="timsChatLoadingErrorDialog">
<p>#{text}</p>
</div>
""").appendTo 'body'
formSubmit = $("""<div class="formSubmit"></div>""").appendTo errorDialog
2013-05-24 17:26:29 +00:00
reloadButton = $("""<button class="buttonPrimary">#{WCF.Language.get 'chat.error.reload'}</button>""").appendTo formSubmit
2013-05-24 13:55:52 +00:00
reloadButton.on 'click', ->
window.location.reload()
$('#timsChatLoadingErrorDialog').wcfDialog
closable: false
title: WCF.Language.get('wcf.global.error.title')
2013-05-15 19:55:51 +00:00
Bind the given callback to the given event.
2013-05-15 19:55:51 +00:00
addListener = (event, callback) ->
return false unless events[event]?
2013-05-24 13:55:52 +00:00
2013-05-15 19:55:51 +00:00
events[event].add callback
2013-05-15 19:55:51 +00:00
Remove the given callback from the given event.
2013-05-15 19:55:51 +00:00
removeListener = (event, callback) ->
return false unless events[event]?
2013-05-15 19:55:51 +00:00
events[event].remove callback
2013-05-15 19:55:51 +00:00
And finally export the public methods and variables.
Chat =
init: init
getMessages: getMessages
refreshRoomList: refreshRoomList
insertText: insertText
freeTheFish: freeTheFish
handleMessages: handleMessages
listener:
add: addListener
remove: removeListener
2013-05-15 19:55:51 +00:00
window.be ?= {}
be.bastelstu ?= {}
window.be.bastelstu.Chat = Chat
)(jQuery, @)