1
0
mirror of https://github.com/wbbaddons/Tims-Chat.git synced 2024-12-22 21:40:08 +00:00

Rework CoffeeScript

This commit is contained in:
Tim Düsterhus 2013-05-15 21:55:51 +02:00
parent 37d8542eff
commit abc6ba66f4
4 changed files with 580 additions and 658 deletions

View File

@ -1,5 +1,8 @@
Main JavaScript file for Tims Chat 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 ### Copyright Information
# @author Tim Düsterhus # @author Tim Düsterhus
@ -8,20 +11,14 @@ Main JavaScript file for Tims Chat
# @package be.bastelstu.chat # @package be.bastelstu.chat
### ###
Setup ## Code
----- We start by setting up our environment by ensuring some sane values for both `$` and `window`,
Ensure sane values for `$` and `window` enabling EMCAScript 5 strict mode, creating the namespace object and overwriting console to prepend
the name of the class.
(($, window) -> (($, window) ->
# Enable strict mode
"use strict"; "use strict";
# Ensure our namespace is present
window.be ?= {}
be.bastelstu ?= {}
Overwrite `console` to add the origin in front of the message
console = console =
log: (message) -> log: (message) ->
window.console.log "[be.bastelstu.Chat] #{message}" window.console.log "[be.bastelstu.Chat] #{message}"
@ -29,163 +26,146 @@ Overwrite `console` to add the origin in front of the message
window.console.warn "[be.bastelstu.Chat] #{message}" window.console.warn "[be.bastelstu.Chat] #{message}"
error: (message) -> error: (message) ->
window.console.error "[be.bastelstu.Chat] #{message}" window.console.error "[be.bastelstu.Chat] #{message}"
be.bastelstu.Chat
=================
be.bastelstu.Chat = Class.extend We continue with defining the needed variables. All variables are local to our closure and will be
exposed by a function if necessary.
Attributes isActive = true
---------- newMessageCount = 0
When `shields` reaches zero `@pe.getMessages` is stopped, to prevent annoying the server with requests that don't go through. Decreased every time `@getMessages()` fails. remainingFailures = 3
shields: 3 events =
Prevents loading messages in parallel.
loading: false
Instances of `WCF.Template`
titleTemplate: null
messageTemplate: null
userTemplate: null
Attributes needed for notificationss
newMessageCount: null
isActive: true
Attributes needed for autocompleter
autocomplete:
offset: 0
value: null
caret: 0
Attributes needed for automated scrolling
oldScrollTop: null
Events one can listen to. Allows 3rd party developers to change data shown in the chat by appending a callback.
events:
newMessage: $.Callbacks() newMessage: $.Callbacks()
userMenu: $.Callbacks() userMenu: $.Callbacks()
submit: $.Callbacks() submit: $.Callbacks()
Every `WCF.PeriodicalExecuter` used by the chat to allow access for 3rd party developers. pe =
pe:
getMessages: null getMessages: null
refreshRoomList: null refreshRoomList: null
fish: null fish: null
Methods loading = false
-------
**init(@config, @titleTemplate, @messageTemplate, @userTemplate)** autocomplete =
Constructor, binds needed events and initializes `@events` and `PeriodicalExecuter`s. 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
init: (@config, @titleTemplate, @messageTemplate, @userTemplate) ->
console.log 'Initializing' console.log 'Initializing'
Bind events and initialize our own event system. When **Tims Chat** becomes focused mark the chat as active and remove the number of new messages from the title.
@events = $(window).focus ->
newMessage: $.Callbacks() document.title = v.titleTemplate.fetch
userMenu: $.Callbacks()
submit: $.Callbacks()
@bindEvents()
@events.newMessage.add $.proxy @notify, @
Initialize `PeriodicalExecuter` and run them once.
@pe.refreshRoomList = new WCF.PeriodicalExecuter $.proxy(@refreshRoomList, @), 60e3
@pe.getMessages = new WCF.PeriodicalExecuter $.proxy(@getMessages, @), @config.reloadTime * 1e3
@refreshRoomList()
@getMessages()
Initialize `nodePush`
@initPush()
Finished! Enable the input now.
$('#timsChatInput').enable().jCounter().focus();
console.log 'Finished initializing - Shields at 104 percent'
**autocomplete(firstChars, offset = @autocompleteOffset)**
Autocompletes a username based on the `firstChars` given and the given `offset`. `offset` allows to skip users.
autocompleter: (firstChars, offset = @autocomplete.offset) ->
Create an array of active chatters with usernames beginning with `firstChars`
users = [ ]
for user in $ '.timsChatUser'
username = $(user).data 'username'
if username.indexOf(firstChars) is 0
users.push username
If no matching user is found return `firstChars`, return the user at the given `offset` with a trailing comma otherwise.
return if users.length is 0 then firstChars else users[offset % users.length] + ','
**bindEvents()**
Binds needed DOM events.
bindEvents: ->
Mark chat as `@isActive` and reset `document.title` to default title, thus removing the number of new messages.
$(window).focus =>
document.title = @titleTemplate.fetch
title: $('#timsChatRoomList .active a').text() title: $('#timsChatRoomList .active a').text()
@newMessageCount = 0
@isActive = true
Mark chat as inactive, thus enabling notifications. newMessageCount = 0
isActive = true
$(window).blur => When **Tims Chat** loses the focus mark the chat as inactive.
@isActive = false
Calls the unload handler (`@unload`) before unloading the chat. $(window).blur ->
isActive = false
$(window).on 'beforeunload', => Make the user leave the chat when **Tims Chat** is about to be unloaded.
@unload()
$(window).on 'beforeunload', ->
new WCF.Action.Proxy
autoSend: true
data:
actionName: 'leave'
className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false
async: false
suppressErrors: true
undefined undefined
Inserts a smiley into the input. Insert the appropriate smiley code into the input when a smiley is clicked.
$('#smilies').on 'click', 'img', (event) => $('#smilies').on 'click', 'img', ->
@insertText ' ' + $(event.target).attr('alt') + ' ' insertText ' ' + $(@).attr('alt') + ' '
Calls the submit handler (`@submit`) when the `#timsChatForm` is `submit`ted. 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) => $('#timsChatForm').submit (event) ->
event.preventDefault() event.preventDefault()
@submit $ event.target
text = $('#timsChatInput').val().trim()
$('#timsChatInput').val('').focus().keyup()
Autocompletes a username when TAB is pressed. return false if text.length is 0
$('#timsChatInput').keydown (event) => # 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 if event.keyCode is $.ui.keyCode.TAB
input = $(event.currentTarget) input = $(event.currentTarget)
event.preventDefault() event.preventDefault()
Calculate `firstChars` to autocomplete, based on the caret position. autocomplete.value ?= input.val()
autocomplete.caret ?= input.getCaret()
@autocomplete.value ?= input.val() beforeCaret = autocomplete.value.substring 0, autocomplete.caret
@autocomplete.caret ?= input.getCaret()
beforeCaret = @autocomplete.value.substring 0, @autocomplete.caret
lastSpace = beforeCaret.lastIndexOf ' ' lastSpace = beforeCaret.lastIndexOf ' '
beforeComplete = @autocomplete.value.substring 0, lastSpace + 1 beforeComplete = autocomplete.value.substring 0, lastSpace + 1
toComplete = @autocomplete.value.substring lastSpace + 1 toComplete = autocomplete.value.substring lastSpace + 1
nextSpace = toComplete.indexOf ' ' nextSpace = toComplete.indexOf ' '
if nextSpace is -1 if nextSpace is -1
afterComplete = ''; afterComplete = '';
@ -196,42 +176,40 @@ Calculate `firstChars` to autocomplete, based on the caret position.
return if toComplete.length is 0 return if toComplete.length is 0
console.log "Autocompleting '#{toComplete}'" console.log "Autocompleting '#{toComplete}'"
Insert completed value into `#timsChatInput` users = (username for user in $('.timsChatUser') when (username = $(user).data('username')).indexOf(toComplete) is 0)
name = @autocompleter toComplete toComplete = users[autocomplete.offset++ % users.length] + ', ' if users.length isnt 0
input.val "#{beforeComplete}#{name} #{afterComplete}" input.val "#{beforeComplete}#{toComplete}#{afterComplete}"
input.setCaret (beforeComplete + name).length + 1 input.setCaret (beforeComplete + toComplete).length
@autocompleteOffset++
Resets autocompleter to default status, when a key is pressed that is not TAB. Reset autocompleter to default status, when a key is pressed that is not TAB.
else else
@autocomplete.offset = 0 $('#timsChatInput').click()
@autocomplete.value = null
@autocomplete.caret = null
Resets autocompleter to default status, when input is `click`ed, as the position of the caret may have changed. Reset autocompleter to default status, when the input is `click`ed, as the position of the caret may have changed.
$('#timsChatInput').click => $('#timsChatInput').click ->
@autocomplete.offset = 0 autocomplete =
@autocomplete.value = null offset: 0
@autocomplete.caret = null value: null
caret: null
Refreshes the room list when the associated button is `click`ed. Refresh the room list when the associated button is `click`ed.
$('#timsChatRoomList button').click => $('#timsChatRoomList button').click ->
@refreshRoomList() @refreshRoomList()
Clears the chat, by removing every single message. Clear the chat by removing every single message once the clear button is `clicked`.
$('#timsChatClear').click (event) => $('#timsChatClear').click (event) ->
event.preventDefault() event.preventDefault()
$('.timsChatMessage').remove() $('.timsChatMessage').remove()
@oldScrollTop = null oldScrollTop = null
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height()
Handling toggling when a toggable button is `click`ed. Handle toggling of the toggleable buttons.
$('.timsChatToggle').click (event) -> $('.timsChatToggle').click (event) ->
element = $ @ element = $ @
@ -246,7 +224,7 @@ Handling toggling when a toggable button is `click`ed.
$('#timsChatInput').focus() $('#timsChatInput').focus()
Mark smilies as disabled. Mark smilies as disabled when they are disabled.
$('#timsChatSmilies').click (event) -> $('#timsChatSmilies').click (event) ->
if $(@).data 'status' if $(@).data 'status'
@ -256,8 +234,8 @@ Mark smilies as disabled.
Toggle fullscreen mode. Toggle fullscreen mode.
$('#timsChatFullscreen').click (event) => $('#timsChatFullscreen').click (event) ->
@oldScrollTop = null if $('#timsChatAutoscroll').data 'status' oldScrollTop = null if $('#timsChatAutoscroll').data 'status'
if $('#timsChatFullscreen').data 'status' if $('#timsChatFullscreen').data 'status'
$('html').addClass 'fullscreen' $('html').addClass 'fullscreen'
else else
@ -267,7 +245,12 @@ Toggle fullscreen mode.
Toggle checkboxes Toggle checkboxes
$('#timsChatMark').click (event) -> $('#timsChatMark').click (event) ->
$('.timsChatMessageContainer').toggleClass 'markEnabled' 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) -> $(document).on 'click', '.timsChatMessage :checkbox', (event) ->
if $(@).is ':checked' if $(@).is ':checked'
@ -275,13 +258,19 @@ Toggle checkboxes
else else
$(@).parents('.timsChatMessage').removeClass('jsMarked') $(@).parents('.timsChatMessage').removeClass('jsMarked')
Scroll down when autoscroll is being activated. Scroll down when autoscroll is being activated.
$('#timsChatAutoscroll').click (event) => $('#timsChatAutoscroll').click (event) ->
if $('#timsChatAutoscroll').data 'status' if $('#timsChatAutoscroll').data 'status'
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height()
@oldScrollTop = $('.timsChatMessageContainer').scrollTop() 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. Ask for permissions to use Desktop notifications when notifications are activated.
@ -292,71 +281,44 @@ Ask for permissions to use Desktop notifications when notifications are activate
window.Notification.requestPermission (permission) -> window.Notification.requestPermission (permission) ->
window.Notification.permission ?= permission window.Notification.permission ?= permission
**changeRoom(target)** events.newMessage.add notify
Change the active chatroom. `target` is the link clicked.
changeRoom: (target) -> Initialize the `PeriodicalExecuter`s and run them once.
Update URL to target URL by using `window.history.replaceState()`. pe.refreshRoomList = new WCF.PeriodicalExecuter refreshRoomList, 60e3
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
refreshRoomList()
getMessages()
window.history.replaceState {}, '', target.attr('href')
$.ajax target.attr('href'), Initialize the [**nodePush**](https://github.com/wbbaddons/nodePush) integration of **Tims Chat**. Once
dataType: 'json' the browser is connected to **nodePush** periodic message loading will be disabled and **Tims Chat** will
data: load messages if the appropriate event arrives.
ajax: 1
type: 'POST'
success: (data, textStatus, jqXHR) =>
@loading = false
target.parent().removeClass 'loading'
# Mark as active do ->
$('.active .timsChatRoom').parent().removeClass 'active' be.bastelstu.wcf.nodePush.onConnect ->
target.parent().addClass 'active' console.log 'Disabling periodic loading'
pe.getMessages.stop()
Update topic, hiding and showing the topic container when necessary. be.bastelstu.wcf.nodePush.onDisconnect ->
console.log 'Enabling periodic loading'
getMessages()
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
$('#timsChatTopic').text data.topic be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.chat.newMessage', getMessages
if data.topic is '' be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.wcf.nodePush.tick60', getMessages
$('#timsChatTopic').addClass 'empty'
else
$('#timsChatTopic').removeClass 'empty'
Mark old messages as `unloaded`. Finished! Enable the input now.
$('.timsChatMessage').addClass 'unloaded' $('#timsChatInput').enable().jCounter().focus();
Show the messages written before entering the room to get a quick glance at the current topic. console.log "Finished initializing"
@handleMessages data.messages true
Update `document.title` to reflect the cnew room. Free the fish.
document.title = @titleTemplate.fetch data freeTheFish = ->
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'
**freeTheFish()**
Free the fish!
freeTheFish: ->
return if $.wcfIsset 'fish' return if $.wcfIsset 'fish'
console.warn 'Freeing the fish' console.warn 'Freeing the fish'
fish = $ """<div id="fish">#{WCF.String.escapeHTML('><((((\u00B0>')}</div>""" fish = $ """<div id="fish">#{WCF.String.escapeHTML('><((((\u00B0>')}</div>"""
@ -369,7 +331,7 @@ Free the fish!
zIndex: 9999 zIndex: 9999
fish.appendTo $ 'body' fish.appendTo $ 'body'
@pe.fish = new WCF.PeriodicalExecuter () -> pe.fish = new WCF.PeriodicalExecuter ->
left = Math.random() * 100 - 50 left = Math.random() * 100 - 50
top = Math.random() * 100 - 50 top = Math.random() * 100 - 50
fish = $ '#fish' fish = $ '#fish'
@ -377,8 +339,10 @@ Free the fish!
left *= -1 unless fish.width() < (fish.position().left + left) < ($(document).width() - fish.width()) 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()) top *= -1 unless fish.height() < (fish.position().top + top) < ($(document).height() - fish.height())
if left > 0
fish.text '><((((\u00B0>' if left > 0 fish.text '><((((\u00B0>' if left > 0
fish.text '<\u00B0))))><' if left < 0 else if left < 0
fish.text '<\u00B0))))><'
fish.animate fish.animate
top: "+=#{top}" top: "+=#{top}"
@ -386,107 +350,77 @@ Free the fish!
, 1e3 , 1e3
, 1.5e3 , 1.5e3
**getMessages()** Fetch new messages from the server and pass them to `handleMessages`. The userlist will be passed to `handleUsers`.
Loads new messages. `remainingFailures` will be decreased on failure and message loading will be entirely disabled once it reaches zero.
getMessages: -> getMessages = ->
$.ajax @config.messageURL, $.ajax v.config.messageURL,
dataType: 'json' dataType: 'json'
type: 'POST' type: 'POST'
success: (data) ->
Handle reply. remainingFailures = 3
success: (data, textStatus, jqXHR) =>
WCF.DOMNodeInsertedHandler.enable() WCF.DOMNodeInsertedHandler.enable()
@handleMessages data.messages handleMessages data.messages
@handleUsers data.users handleUsers data.users
WCF.DOMNodeInsertedHandler.disable() WCF.DOMNodeInsertedHandler.disable()
error: ->
console.error "Message loading failed, #{--remainingFailures} remaining"
if remainingFailures <= 0
loading = true
Decrease `@shields` on error and disable PeriodicalExecuters once `@shields` reaches zero. pe.refreshRoomList.stop()
pe.getMessages.stop()
freeTheFish()
console.error 'To many failues, aborting'
error: => $("""<div id="timsChatLoadingErrorDialog">#{WCF.Language.get('chat.general.error.onMessageLoad')}</div>""").appendTo('body') unless $.wcfIsset('timsChatLoadingErrorDialog')
console.error 'Battle Station hit - shields at ' + (--@shields / 3 * 104) + ' percent'
if @shields <= 0
@pe.refreshRoomList.stop()
@pe.getMessages.stop()
@freeTheFish()
console.error 'We got destroyed, but could free our friend the fish before he was killed as well. Have a nice life in freedom!'
$("""<div id="timsChatLoadingErrorDialog">#{WCF.Language.get('chat.general.error.onMessageLoad')}</div>""").appendTo('body') if not $.wcfIsset('timsChatLoadingErrorDialog')
$('#timsChatLoadingErrorDialog').wcfDialog $('#timsChatLoadingErrorDialog').wcfDialog
closable: false, closable: false
title: WCF.Language.get('wcf.global.error.title') title: WCF.Language.get('wcf.global.error.title')
complete: => complete: ->
@loading = false loading = false
Prevent loading messages in parallel, as this leads to several problems. Prevent loading messages in parallel.
beforeSend: => beforeSend: ->
return false if @loading return false if loading
@loading = true loading = true
**handleMessages(messages)** Insert the given messages into the chat stream.
Inserts the `messages` given into the stream.
handleMessages: (messages) -> handleMessages = (messages) ->
$('#timsChatMessageContainer').trigger 'scroll'
Disable autoscroll when the user scrolled up to read old messages
unless @oldScrollTop is null
if $('#timsChatMessageContainer').scrollTop() < @oldScrollTop
if $('#timsChatAutoscroll').data('status') is 1
$('#timsChatAutoscroll').click()
$('#timsChatAutoscroll').parent().fadeOut('slow').fadeIn 'slow'
Insert the new messages.
for message in messages for message in messages
events.newMessage.fire message
Prevent problems with race condition output = v.messageTemplate.fetch message
continue if $.wcfIsset "timsChatMessage#{message.messageID}"
Call the `@events.newMessage` event.
@events.newMessage.fire message
Build HTML of the message and append it to our current message list
output = @messageTemplate.fetch message
li = $ '<li></li>' li = $ '<li></li>'
li.attr 'id', "timsChatMessage#{message.messageID}" li.addClass 'timsChatMessage'
li.addClass 'timsChatMessage timsChatMessage'+message.type li.addClass "timsChatMessage#{message.type}"
li.addClass "user#{message.sender}"
li.addClass 'ownMessage' if message.sender is WCF.User.userID li.addClass 'ownMessage' if message.sender is WCF.User.userID
li.append output li.append output
li.appendTo $ '#timsChatMessageContainer > ul' li.appendTo $ '#timsChatMessageContainer > ul'
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').prop('scrollHeight') if $('#timsChatAutoscroll').data('status') is 1
oldScrollTop = $('#timsChatMessageContainer').scrollTop()
Scroll down when autoscrolling is enabled. Rebuild the userlist based on the given `users`.
$('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() if $('#timsChatAutoscroll').data('status') is 1
@oldScrollTop = $('#timsChatMessageContainer').scrollTop()
**handleUsers(users)**
Rebuild the userlist containing `users` afterwards.
handleUsers: (users) ->
Keep track of the users that did not leave.
handleUsers = (users) ->
foundUsers = { } foundUsers = { }
Loop all users.
for user in users for user in users
id = "timsChatUser-#{user.userID}" id = "timsChatUser-#{user.userID}"
element = $ "##{id}" element = $ "##{id}"
Move the user, to prevent rebuilding the entire user list. Move the user to the new position if he was found in the old list.
if element[0] if element.length
console.log "Moving User: '#{user.username}'" console.log "Moving User: '#{user.username}'"
element = element.detach() element = element.detach()
@ -505,7 +439,7 @@ Move the user, to prevent rebuilding the entire user list.
$('#timsChatUserList > ul').append element $('#timsChatUserList > ul').append element
Build HTML of new user and append it. Build HTML of the user and insert it into the list, if the users was not found in the chat before.
else else
console.log "Inserting User: '#{user.username}'" console.log "Inserting User: '#{user.username}'"
@ -521,7 +455,7 @@ Build HTML of new user and append it.
li.attr 'title', user.awayStatus li.attr 'title', user.awayStatus
li.data 'username', user.username li.data 'username', user.username
li.append @userTemplate.fetch user li.append v.userTemplate.fetch user
menu = $ '<ul></ul>' menu = $ '<ul></ul>'
menu.addClass 'dropdownMenu' menu.addClass 'dropdownMenu'
@ -529,7 +463,7 @@ Build HTML of new user and append it.
menu.append $ "<li><a>#{WCF.Language.get('chat.general.kick')}</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>#{WCF.Language.get('chat.general.ban')}</a></li>"
menu.append $ """<li><a href="#{user.link}">#{WCF.Language.get('chat.general.profile')}</a></li>""" menu.append $ """<li><a href="#{user.link}">#{WCF.Language.get('chat.general.profile')}</a></li>"""
@events.userMenu.fire user, menu events.userMenu.fire user, menu
li.append menu li.append menu
li.appendTo $ '#timsChatUserList > ul' li.appendTo $ '#timsChatUserList > ul'
@ -538,7 +472,7 @@ Build HTML of new user and append it.
Remove all users that left the chat. Remove all users that left the chat.
$('.timsChatUser').each () -> $('.timsChatUser').each ->
unless foundUsers[$(@).attr('id')]? unless foundUsers[$(@).attr('id')]?
console.log "Removing User: '#{$(@).data('username')}'" console.log "Removing User: '#{$(@).data('username')}'"
$(@).remove(); $(@).remove();
@ -546,40 +480,14 @@ Remove all users that left the chat.
$('#toggleUsers .badge').text $('.timsChatUser').length $('#toggleUsers .badge').text $('.timsChatUser').length
**initPush()** Insert the given `text` into the input. If `options.append` is true the given `text` will be appended, otherwise it will replaced
Initialize socket.io to enable nodePush. the existing text. If `options.submit` is true the message will be sent to the server afterwards.
initPush: -> insertText = (text, options = { }) ->
be.bastelstu.wcf.nodePush.onConnect =>
console.log 'Disabling periodic loading'
Disable `@pe.getMessages` once we are connected.
@pe.getMessages.stop()
be.bastelstu.wcf.nodePush.onDisconnect =>
console.log 'Enabling periodic loading'
@getMessages()
Reenable `@pe.getMessages` once we are disconnected.
@pe.getMessages = new WCF.PeriodicalExecuter $.proxy(@getMessages, @), @config.reloadTime * 1e3
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.chat.newMessage', =>
@getMessages()
be.bastelstu.wcf.nodePush.onMessage 'be.bastelstu.wcf.nodePush.tick60', =>
@getMessages()
**insertText(text, options)**
Inserts the given `text` into the input. If `options.append` is truthy the given `text` will be appended and replaces the existing text otherwise. If `options.submit` is truthy the message will be submitted afterwards.
insertText: (text, options) ->
options = $.extend options = $.extend
append: true append: true
submit: false submit: false
, options or {} , options
text = $('#timsChatInput').val() + text if options.append text = $('#timsChatInput').val() + text if options.append
$('#timsChatInput').val text $('#timsChatInput').val text
@ -590,22 +498,21 @@ Inserts the given `text` into the input. If `options.append` is truthy the given
else else
$('#timsChatInput').focus() $('#timsChatInput').focus()
**notify(message)**
Sends 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) -> 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.
return if @isActive or $('#timsChatNotify').data('status') is 0
@newMessageCount++
document.title = '(' + @newMessageCount + ') ' + @titleTemplate.fetch notify = (message) ->
return if isActive or $('#timsChatNotify').data('status') is 0
document.title = v.titleTemplate.fetch
title: $('#timsChatRoomList .active a').text() title: $('#timsChatRoomList .active a').text()
newMessageCount: ++newMessageCount
# Desktop Notifications # Desktop Notifications
title = WCF.Language.get 'chat.general.notify.title' title = WCF.Language.get 'chat.general.notify.title'
content = "#{message.username}#{message.separator} #{message.message}" content = "#{message.username}#{message.separator} #{message.message}"
if window.Notification? if window.Notification?.permission is 'granted'
if window.Notification.permission is 'granted'
do -> do ->
notification = new window.Notification title, notification = new window.Notification title,
body: content body: content
@ -615,21 +522,20 @@ Sends out notifications for the given `message`. The number of unread messages w
notification.close() notification.close()
, 5e3 , 5e3
**refreshRoomList()** Fetch the roomlist from the server and update it in the GUI.
Updates the room list.
refreshRoomList: -> refreshRoomList = ->
console.log 'Refreshing the roomlist' console.log 'Refreshing the roomlist'
$('#toggleRooms .ajaxLoad').show() $('#toggleRooms .ajaxLoad').show()
proxy = new WCF.Action.Proxy new WCF.Action.Proxy
autoSend: true autoSend: true
data: data:
actionName: 'getRoomList' actionName: 'getRoomList'
className: 'chat\\data\\room\\RoomAction' className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false showLoadingOverlay: false
suppressErrors: true suppressErrors: true
success: (data) => success: (data) ->
$('#timsChatRoomList li').remove() $('#timsChatRoomList li').remove()
$('#toggleRooms .ajaxLoad').hide() $('#toggleRooms .ajaxLoad').hide()
$('#toggleRooms .badge').text data.returnValues.length $('#toggleRooms .badge').text data.returnValues.length
@ -640,69 +546,87 @@ Updates the room list.
$("""<a href="#{room.link}">#{room.title}</a>""").addClass('timsChatRoom').appendTo li $("""<a href="#{room.link}">#{room.title}</a>""").addClass('timsChatRoom').appendTo li
$('#timsChatRoomList ul').append li $('#timsChatRoomList ul').append li
Bind click event for inline room change if we have the history API available.
if window.history?.replaceState? if window.history?.replaceState?
$('.timsChatRoom').click (event) => $('.timsChatRoom').click (event) ->
event.preventDefault() event.preventDefault()
@changeRoom $ event.target 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" console.log "Found #{data.returnValues.length} rooms"
**submit(target)** Bind the given callback to the given event.
Submits the message.
submit: (target) -> addListener = (event, callback) ->
# Break if input contains only whitespace return false unless events[event]?
return false if $('#timsChatInput').val().trim().length is 0
# Free the fish! events[event].add callback
@freeTheFish() if $('#timsChatInput').val().trim().toLowerCase() is '/free the fish'
text = $('#timsChatInput').val() Remove the given callback from the given event.
# call submit event removeListener = (event, callback) ->
text = do (text) => return false unless events[event]?
obj =
text: text
@events.submit.fire obj
obj.text events[event].remove callback
$('#timsChatInput').val('').focus().keyup() And finally export the public methods and variables.
proxy = new WCF.Action.Proxy Chat =
autoSend: true init: init
data: getMessages: getMessages
actionName: 'send' refreshRoomList: refreshRoomList
className: 'chat\\data\\message\\MessageAction' insertText: insertText
parameters: freeTheFish: freeTheFish
text: text handleMessages: handleMessages
enableSmilies: $('#timsChatSmilies').data 'status' listener:
showLoadingOverlay: false add: addListener
success: => remove: removeListener
$('#timsChatInputContainer').removeClass('formError').find('.innerError').hide()
@getMessages()
failure: (data) =>
return true if not (data?.returnValues?.errorType?) and not (data?.message?)
$('#timsChatInputContainer').addClass('formError').find('.innerError').show().html (data?.returnValues?.errorType) ? data.message
setTimeout ->
$('#timsChatInputContainer').removeClass('formError').find('.innerError').hide().html("")
, 3000
false window.be ?= {}
be.bastelstu ?= {}
**unload()** window.be.bastelstu.Chat = Chat
Sends leave notification to the server.
unload: ->
proxy = new WCF.Action.Proxy
autoSend: true
data:
actionName: 'leave'
className: 'chat\\data\\room\\RoomAction'
showLoadingOverlay: false
async: false
suppressErrors: true
)(jQuery, @) )(jQuery, @)

View File

@ -28,12 +28,10 @@ public static function getNewestMessages(\chat\data\room\Room $room, $number = C
$messageList->sqlOrderBy = "message.messageID DESC"; $messageList->sqlOrderBy = "message.messageID DESC";
$messageList->sqlLimit = $number; $messageList->sqlLimit = $number;
$messageList->getConditionBuilder()->add(' $messageList->getConditionBuilder()->add('
(( (
message.receiver IS NULL message.receiver IS NULL
AND message.roomID = ? AND message.roomID = ?
) )', array($room->roomID));
OR message.receiver = ?
OR message.sender = ?)', array($room->roomID, \wcf\system\WCF::getUser()->userID, \wcf\system\WCF::getUser()->userID));
$messageList->readObjects(); $messageList->readObjects();
return array_reverse($messageList->getObjects()); return array_reverse($messageList->getObjects());

View File

@ -26,12 +26,12 @@
{capture assign='messageTemplate'}{include application='chat' file='message'}{/capture} {capture assign='messageTemplate'}{include application='chat' file='message'}{/capture}
{capture assign='userTemplate'}{include application='chat' file='userListUser'}{/capture} {capture assign='userTemplate'}{include application='chat' file='userListUser'}{/capture}
window.chat = new be.bastelstu.Chat( be.bastelstu.Chat.init(
{ {
reloadTime: {@CHAT_RELOADTIME}, reloadTime: {@CHAT_RELOADTIME},
messageURL: '{link application="chat" controller="NewMessages"}{/link}' messageURL: '{link application="chat" controller="NewMessages"}{/link}'
}, },
new WCF.Template('{ldelim}$title} - {"chat.general.title"|language|encodeJS} - {PAGE_TITLE|language|encodeJS}'), new WCF.Template('{literal}{if $newMessageCount}({#$newMessageCount}) {/if}{$title} - {/literal}{"chat.general.title"|language|encodeJS} - {PAGE_TITLE|language|encodeJS}'),
new WCF.Template('{@$messageTemplate|encodeJS}'), new WCF.Template('{@$messageTemplate|encodeJS}'),
new WCF.Template('{@$userTemplate|encodeJS}') new WCF.Template('{@$userTemplate|encodeJS}')
); );
@ -39,7 +39,7 @@
{event name='afterInit'} {event name='afterInit'}
// show the last X messages // show the last X messages
window.chat.handleMessages([ be.bastelstu.Chat.handleMessages([
{implode from=$newestMessages item='message'}{@$message->jsonify()}{/implode} {implode from=$newestMessages item='message'}{@$message->jsonify()}{/implode}
]); ]);

View File

@ -1,3 +1,3 @@
<script type="text/javascript" src="{$__wcf->getPath('chat')}js/be.bastelstu.Chat.js?version={PACKAGE_VERSION|rawurlencode}"></script> <script type="text/javascript" src="{$__wcf->getPath('chat')}js/be.bastelstu.Chat.js?version={PACKAGE_VERSION|rawurlencode}"></script>
<script type="text/javascript" src="{$__wcf->getPath('chat')}js/be.bastelstu.Chat.Log.js?version={PACKAGE_VERSION|rawurlencode}"></script> <!--script type="text/javascript" src="{$__wcf->getPath('chat')}js/be.bastelstu.Chat.Log.js?version={PACKAGE_VERSION|rawurlencode}"></script-->
{event name='javascript'} {event name='javascript'}