1
0
mirror of https://github.com/wbbaddons/Tims-Chat.git synced 2024-12-22 21:40:08 +00:00
Tims-Chat/file/js/be.bastelstu.Chat.litcoffee
2013-05-15 21:55:51 +02:00

633 lines
19 KiB
Plaintext

Tims Chat 3
===========
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
# @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>
# @package be.bastelstu.chat
###
## Code
We start by setting up our environment by ensuring some sane values for both `$` and `window`,
enabling EMCAScript 5 strict mode, creating the namespace object 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}"
We continue with defining the needed variables. All variables are local to our closure and will be
exposed by a function if necessary.
isActive = true
newMessageCount = 0
remainingFailures = 3
events =
newMessage: $.Callbacks()
userMenu: $.Callbacks()
submit: $.Callbacks()
pe =
getMessages: null
refreshRoomList: null
fish: null
loading = false
autocomplete =
offset: 0
value: null
caret: 0
oldScrollTop = null
v =
titleTemplate: null
messageTemplate: null
userTemplate: null
config: null
Initialize **Tims Chat**. Bind needed DOM events and initialize data structures.
initialized = false
init = (config, titleTemplate, messageTemplate, userTemplate) ->
return false if initialized
initialized = true
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.
$(window).focus ->
document.title = v.titleTemplate.fetch
title: $('#timsChatRoomList .active a').text()
newMessageCount = 0
isActive = true
When **Tims Chat** loses the focus mark the chat as inactive.
$(window).blur ->
isActive = false
Make the user leave the chat when **Tims Chat** is about to be unloaded.
$(window).on 'beforeunload', ->
new WCF.Action.Proxy
autoSend: true
data:
actionName: 'leave'
className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false
async: false
suppressErrors: true
undefined
Insert the appropriate smiley code into the input when a smiley is clicked.
$('#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.
$('#timsChatForm').submit (event) ->
event.preventDefault()
text = $('#timsChatInput').val().trim()
$('#timsChatInput').val('').focus().keyup()
return false if text.length is 0
# Free the fish!
freeTheFish() if text.toLowerCase() is '/free the fish'
text = do (text) ->
obj =
text: text
events.submit.fire obj
obj.text
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
false
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.
$('#timsChatInput').keydown (event) ->
if event.keyCode is $.ui.keyCode.TAB
input = $(event.currentTarget)
event.preventDefault()
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}'"
users = (username for user in $('.timsChatUser') when (username = $(user).data('username')).indexOf(toComplete) is 0)
toComplete = users[autocomplete.offset++ % users.length] + ', ' if users.length isnt 0
input.val "#{beforeComplete}#{toComplete}#{afterComplete}"
input.setCaret (beforeComplete + toComplete).length
Reset autocompleter to default status, when a key is pressed that is not TAB.
else
$('#timsChatInput').click()
Reset autocompleter to default status, when the input is `click`ed, as the position of the caret may have changed.
$('#timsChatInput').click ->
autocomplete =
offset: 0
value: null
caret: null
Refresh the room list when the associated button is `click`ed.
$('#timsChatRoomList button').click ->
@refreshRoomList()
Clear the chat by removing every single message once the clear button is `clicked`.
$('#timsChatClear').click (event) ->
event.preventDefault()
$('.timsChatMessage').remove()
oldScrollTop = null
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height()
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()
Mark smilies as disabled when they are disabled.
$('#timsChatSmilies').click (event) ->
if $(@).data 'status'
$('#smilies').removeClass 'disabled'
else
$('#smilies').addClass 'disabled'
Toggle fullscreen mode.
$('#timsChatFullscreen').click (event) ->
oldScrollTop = null if $('#timsChatAutoscroll').data 'status'
if $('#timsChatFullscreen').data 'status'
$('html').addClass 'fullscreen'
else
$('html').removeClass 'fullscreen'
Toggle checkboxes
$('#timsChatMark').click (event) ->
if $(@).data 'status'
$('.timsChatMessageContainer').addClass 'markEnabled'
else
$('.timsChatMessageContainer').removeClass 'markEnabled'
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')
Scroll down when autoscroll is being activated.
$('#timsChatAutoscroll').click (event) ->
if $('#timsChatAutoscroll').data 'status'
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height()
oldScrollTop = $('.timsChatMessageContainer').scrollTop()
$('#timsChatMessageContainer').on 'scroll', (event) ->
return if oldScrollTop is null
if $(@).scrollTop() < oldScrollTop
if $('#timsChatAutoscroll').data('status') is 1
$('#timsChatAutoscroll').click().parent().fadeOut('slow').fadeIn 'slow'
Ask for permissions to use Desktop notifications when notifications are activated.
if window.Notification?
$('#timsChatNotify').click (event) ->
return unless $(@).data 'status'
if window.Notification.permission isnt 'granted'
window.Notification.requestPermission (permission) ->
window.Notification.permission ?= permission
events.newMessage.add notify
Initialize the `PeriodicalExecuter`s and run them once.
pe.refreshRoomList = new WCF.PeriodicalExecuter refreshRoomList, 60e3
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
refreshRoomList()
getMessages()
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()
be.bastelstu.wcf.nodePush.onDisconnect ->
console.log 'Enabling periodic loading'
getMessages()
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.chat.newMessage', getMessages
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.wcf.nodePush.tick60', getMessages
Finished! Enable the input now.
$('#timsChatInput').enable().jCounter().focus();
console.log "Finished initializing"
true
Free the fish.
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'
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
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
loading = true
pe.refreshRoomList.stop()
pe.getMessages.stop()
freeTheFish()
console.error 'To many failues, aborting'
$("""<div id="timsChatLoadingErrorDialog">#{WCF.Language.get('chat.general.error.onMessageLoad')}</div>""").appendTo('body') unless $.wcfIsset('timsChatLoadingErrorDialog')
$('#timsChatLoadingErrorDialog').wcfDialog
closable: false
title: WCF.Language.get('wcf.global.error.title')
complete: ->
loading = false
Prevent loading messages in parallel.
beforeSend: ->
return false if loading
loading = true
Insert the given messages into the chat stream.
handleMessages = (messages) ->
$('#timsChatMessageContainer').trigger 'scroll'
for message in messages
events.newMessage.fire message
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 ul').prop('scrollHeight') if $('#timsChatAutoscroll').data('status') is 1
oldScrollTop = $('#timsChatMessageContainer').scrollTop()
Rebuild the userlist based on the given `users`.
handleUsers = (users) ->
foundUsers = { }
for user in users
id = "timsChatUser-#{user.userID}"
element = $ "##{id}"
Move the user to the new position if he was found in the old list.
if element.length
console.log "Moving User: '#{user.username}'"
element = element.detach()
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
Build HTML of the user and insert it into the list, if the users was not found in the chat before.
else
console.log "Inserting User: '#{user.username}'"
li = $ '<li></li>'
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
li.append v.userTemplate.fetch user
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>"""
events.userMenu.fire user, menu
li.append menu
li.appendTo $ '#timsChatUserList > ul'
foundUsers[id] = true
Remove all users that left the chat.
$('.timsChatUser').each ->
unless foundUsers[$(@).attr('id')]?
console.log "Removing User: '#{$(@).data('username')}'"
$(@).remove();
$('#toggleUsers .badge').text $('.timsChatUser').length
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.
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()
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.
notify = (message) ->
return if isActive or $('#timsChatNotify').data('status') is 0
document.title = v.titleTemplate.fetch
title: $('#timsChatRoomList .active a').text()
newMessageCount: ++newMessageCount
# Desktop Notifications
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
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
if window.history?.replaceState?
$('.timsChatRoom').click (event) ->
event.preventDefault()
target = $(@)
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
Fix smiley category URLs, as the URL changed.
$('#smilies .menu li a').each (key, value) ->
anchor = $(value)
anchor.attr 'href', anchor.attr('href').replace /.*#/, "#{target.attr href}#"
Reload the whole page when an error occurs. The users thus sees the error message (usually `PermissionDeniedException`)
error: ->
window.location.reload true
Show loading icon and prevent switching the room in parallel.
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"
Bind the given callback to the given event.
addListener = (event, callback) ->
return false unless events[event]?
events[event].add callback
Remove the given callback from the given event.
removeListener = (event, callback) ->
return false unless events[event]?
events[event].remove callback
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
window.be ?= {}
be.bastelstu ?= {}
window.be.bastelstu.Chat = Chat
)(jQuery, @)