diff --git a/contrib/build.php b/contrib/build.php index eed76ab..9e8e351 100755 --- a/contrib/build.php +++ b/contrib/build.php @@ -35,12 +35,12 @@ ------------------- EOT; -foreach (glob('file/js/*.coffee') as $coffeeFile) { +foreach (glob('file/js/*.{litcoffee,coffee}', GLOB_BRACE) as $coffeeFile) { echo $coffeeFile."\n"; passthru('coffee -cb '.escapeshellarg($coffeeFile), $code); if ($code != 0) exit($code); } -foreach (glob('file/acp/be.bastelstu.chat.nodePush/lib/*.coffee') as $coffeeFile) { +foreach (glob('file/acp/be.bastelstu.chat.nodePush/lib/*.{litcoffee,coffee}', GLOB_BRACE) as $coffeeFile) { echo $coffeeFile."\n"; passthru('coffee -cb '.escapeshellarg($coffeeFile), $code); if ($code != 0) exit($code); diff --git a/file/acp/be.bastelstu.chat.nodePush/lib/server.coffee b/file/acp/be.bastelstu.chat.nodePush/lib/server.coffee deleted file mode 100644 index 1b651d3..0000000 --- a/file/acp/be.bastelstu.chat.nodePush/lib/server.coffee +++ /dev/null @@ -1,69 +0,0 @@ -### -# node.js Pushserver for Tims Chat. -# -# @author Tim Düsterhus -# @copyright 2010-2013 Tim Düsterhus -# @license Creative Commons Attribution-NonCommercial-ShareAlike -# @package be.bastelstu.chat -# @subpackage nodePush -### -process.title = 'nodePush - Tims Chat' - -io = require 'socket.io' -net = require 'net' -fs = require 'fs' - -config = require '../config.js' - -log = (message) -> - console.log "[be.bastelstu.chat.nodePush] #{message}" - -class Server - constructor: () -> - log 'Starting Pushserver for Tims Chat' - log "PID is #{process.pid}" - log "Using port: #{config.port}" - - @initUnixSocket() - @initSocketIO() - - process.on 'exit', @shutdown.bind @ - process.on 'uncaughtException', @shutdown.bind @ - process.on 'SIGINT', @shutdown.bind @ - process.on 'SIGTERM', @shutdown.bind @ - - setInterval => - @socket.sockets.emit 'newMessage' - , 60e3 - initSocketIO: () -> - log 'Initializing socket.io' - @socket = io.listen config.port - - @socket.set 'log level', 1 - @socket.set 'browser client etag', true - @socket.set 'browser client minification', true - @socket.set 'browser client gzip', true - - @socket.configure 'development', => - @socket.set 'log level', 3 - @socket.set 'browser client etag', false - @socket.set 'browser client minification', false - initUnixSocket: () -> - log 'Initializing Unix-Socket' - socket = net.createServer (c) => - setTimeout => - @socket.sockets.emit 'newMessage' - , 20 - - c.end() - - socket.listen "#{__dirname}/../data.sock" - fs.chmod "#{__dirname}/../data.sock", '777' - shutdown: () -> - return unless fs.existsSync "#{__dirname}/../data.sock" - - log 'Shutting down' - fs.unlinkSync "#{__dirname}/../data.sock" - process.exit() - -new Server() \ No newline at end of file diff --git a/file/acp/be.bastelstu.chat.nodePush/lib/server.litcoffee b/file/acp/be.bastelstu.chat.nodePush/lib/server.litcoffee new file mode 100644 index 0000000..944a417 --- /dev/null +++ b/file/acp/be.bastelstu.chat.nodePush/lib/server.litcoffee @@ -0,0 +1,109 @@ +nodePush Pushserver for Tims Chat +================================= + +Copyright Information +--------------------- + + "@author Tim Düsterhus" + "@copyright 2010-2013 Tim Düsterhus" + "@license Creative Commons Attribution-NonCommercial-ShareAlike " + "@package be.bastelstu.chat" + "@subpackage nodePush" + +Setup +----- + +Load required namespaces. + + io = require 'socket.io' + net = require 'net' + fs = require 'fs' + +Load config + + config = require '../config.js' + +Prepare environment + + log = (message) -> + console.log "[be.bastelstu.chat.nodePush] #{message}" + +be.bastelstu.chat.nodePush +========================== + + class be.bastelstu.chat.nodePush + +Methods +------- +**constructor()** + + constructor: -> + log 'Starting Pushserver for Tims Chat' + log "PID is #{process.pid}" + log "Using port: #{config.port}" + + @initUnixSocket() + @initSocketIO() + +Bind shutdown function to needed events. + + process.on 'exit', @shutdown.bind @ + process.on 'uncaughtException', @shutdown.bind @ + process.on 'SIGINT', @shutdown.bind @ + process.on 'SIGTERM', @shutdown.bind @ + +Set nice title for PS. + + process.title = 'nodePush - Tims Chat' + +Set newMessage event once a minute to allow for easier timeout detection in chat. + + setInterval => + @socket.sockets.emit 'newMessage' + , 60e3 + +**initSocketIO()** +Initialize socket server. + + initSocketIO: -> + log 'Initializing socket.io' + @socket = io.listen config.port + + @socket.set 'log level', 1 + @socket.set 'browser client etag', true + @socket.set 'browser client minification', true + @socket.set 'browser client gzip', true + + @socket.configure 'development', => + @socket.set 'log level', 3 + @socket.set 'browser client etag', false + @socket.set 'browser client minification', false + +**initUnixSocket()** +Initialize PHP side unix socket. + + initUnixSocket: -> + log 'Initializing Unix-Socket' + socket = net.createServer (c) => + setTimeout => + @socket.sockets.emit 'newMessage' + , 20 + + c.end() + + socket.listen "#{__dirname}/../data.sock" + fs.chmod "#{__dirname}/../data.sock", '777' + +**shutdown()** +Perform clean shutdown of nodePush. + + shutdown: -> + return unless fs.existsSync "#{__dirname}/../data.sock" + + log 'Shutting down' + fs.unlinkSync "#{__dirname}/../data.sock" + process.exit() + +And finally start the service. + + new be.bastelstu.chat.nodePush() \ No newline at end of file diff --git a/file/acp/be.bastelstu.chat.nodePush/package.json b/file/acp/be.bastelstu.chat.nodePush/package.json index 8c8fae0..c778ac2 100644 --- a/file/acp/be.bastelstu.chat.nodePush/package.json +++ b/file/acp/be.bastelstu.chat.nodePush/package.json @@ -3,7 +3,7 @@ "description" : "node.js-Pushing for Tims Chat", "homepage" : "https://github.com/wbbaddons/Tims-Chat", "keywords" : ["chat"], - "author" : "Tim Düsterhus ", + "author" : "Tim Düsterhus ", "contributors" : [ ], "dependencies" : { diff --git a/file/js/be.bastelstu.Chat.coffee b/file/js/be.bastelstu.Chat.coffee deleted file mode 100644 index 80cb6db..0000000 --- a/file/js/be.bastelstu.Chat.coffee +++ /dev/null @@ -1,585 +0,0 @@ -### -# be.bastelstu.WCF.Chat -# -# @author Tim Düsterhus -# @copyright 2010-2013 Tim Düsterhus -# @license Creative Commons Attribution-NonCommercial-ShareAlike -# @package be.bastelstu.chat -### - -window.console ?= - log: () ->, - warn: () ->, - error: () -> - -(($, window, _console) -> - "use strict"; - window.be ?= {} - be.bastelstu ?= {} - - console = - log: (message) -> - _console.log "[be.bastelstu.Chat] #{message}" - warn: (message) -> - _console.warn "[be.bastelstu.Chat] #{message}" - error: (message) -> - _console.error "[be.bastelstu.Chat] #{message}" - - - be.bastelstu.Chat = Class.extend - # Tims Chat stops loading when this reaches zero - # TODO: We need an explosion animation - shields: 3 - - # Are we currently loading messages? - loading: false - - # Templates - titleTemplate: null - messageTemplate: null - - # Notifications - newMessageCount: null - isActive: true - - # Autocompleter - autocompleteOffset: 0 - autocompleteValue: null - autocompleteCaret: 0 - - # Autoscroll - oldScrollTop: null - - # Events - events: - newMessage: $.Callbacks() - userMenu: $.Callbacks() - submit: $.Callbacks() - - # socket.io - socket: null - - pe: - getMessages: null - refreshRoomList: null - fish: null - init: (@config, @titleTemplate, @messageTemplate) -> - console.log 'Initializing' - - @events = - newMessage: $.Callbacks() - userMenu: $.Callbacks() - submit: $.Callbacks() - - @bindEvents() - @events.newMessage.add $.proxy @notify, @ - - @pe.refreshRoomList = new WCF.PeriodicalExecuter $.proxy(@refreshRoomList, @), 60e3 - @pe.getMessages = new WCF.PeriodicalExecuter $.proxy(@getMessages, @), @config.reloadTime * 1e3 - @refreshRoomList() - @getMessages() - @initPush() - - console.log 'Finished initializing - Shields at 104 percent' - ### - # Autocompletes a username - ### - autocomplete: (firstChars, offset = @autocompleteOffset) -> - users = [] - - # Search all matching users - for user in $ '.timsChatUser' - username = $(user).data 'username' - if username.indexOf(firstChars) is 0 - users.push username - - # None found -> return firstChars - # otherwise return the user at the current offset - return if users.length is 0 then firstChars else users[offset % users.length] + ',' - ### - # Binds all the events needed for Tims Chat. - ### - bindEvents: -> - # Mark window as focused - $(window).focus => - document.title = @titleTemplate.fetch - title: $('#timsChatRoomList .activeMenuItem a').text() - @newMessageCount = 0 - @isActive = true - - - # Mark window as blurred - $(window).blur => - @isActive = false - - - # Unload the chat - $(window).on 'beforeunload', => - @unload() - undefined - - - # Insert a smiley - $('#smilies').on 'click', 'img', (event) => - @insertText ' ' + $(event.target).attr('alt') + ' ' - - - # Switch sidebar tab - $('.timsChatSidebarTabs li').click (event) => - event.preventDefault() - @toggleSidebarContents $ event.target - - - # Submit Handler - $('#timsChatForm').submit (event) => - event.preventDefault() - @submit $ event.target - - - # Autocompleter - $('#timsChatInput').keydown (event) => - # tab key - if event.keyCode is 9 - event.preventDefault() - @autocompleteValue = $('#timsChatInput').val() if @autocompleteValue is null - @autocompleteCaret = $('#timsChatInput').getCaret() if @autocompleteCaret is null - - beforeCaret = @autocompleteValue.substring 0, @autocompleteCaret - lastSpace = beforeCaret.lastIndexOf ' ' - beforeComplete = @autocompleteValue.substring 0, lastSpace + 1 - toComplete = @autocompleteValue.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}'" - - # Insert name and increment offset - name = @autocomplete toComplete - - $('#timsChatInput').val "#{beforeComplete}#{name} #{afterComplete}" - $('#timsChatInput').setCaret (beforeComplete + name).length + 1 - @autocompleteOffset++ - else - @autocompleteOffset = 0 - @autocompleteValue = null - @autocompleteCaret = null - - - $('#timsChatInput').click => - @autocompleteOffset = 0 - @autocompleteValue = null - @autocompleteCaret = null - - - # Refreshes the roomlist - $('#timsChatRoomList button').click $.proxy @refreshRoomList, @ - - # Clears the stream - $('#timsChatClear').click (event) -> - event.preventDefault() - $('.timsChatMessage').remove() - @oldScrollTop = null - $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() - - # Toggle Buttons - $('.timsChatToggle').click (event) -> - element = $ @ - icon = element.find 'span.icon' - if element.data('status') is 1 - element.data 'status', 0 - icon.removeClass('icon-circle-blank').addClass('icon-off') - element.attr 'title', element.data 'enableMessage' - else - element.data 'status', 1 - icon.removeClass('icon-off').addClass('icon-circle-blank') - element.attr 'title', element.data 'disableMessage' - - $('#timsChatInput').focus() - - # Enable fullscreen-mode - $('#timsChatFullscreen').click (event) -> - if $(@).data 'status' - $('html').addClass 'fullscreen' - else - $('html').removeClass 'fullscreen' - - # Immediatly scroll down when activating autoscroll - $('#timsChatAutoscroll').click (event) -> - $(@).removeClass 'active' - if $(@).data 'status' - $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() - @oldScrollTop = $('.timsChatMessageContainer').scrollTop() - - # Desktop Notifications - if window.Notification? - $('#timsChatNotify').click (event) -> - return unless $(@).data 'status' - if window.Notification.permission isnt 'granted' - window.Notification.requestPermission (permission) -> - window.Notification.permission ?= permission - ### - # Changes the chat-room. - # - # @param jQuery-object target - ### - changeRoom: (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' - - # Mark as active - $('.activeMenuItem .timsChatRoom').parent().removeClass 'activeMenuItem' - target.parent().addClass 'activeMenuItem' - - # Set new topic - $('#timsChatTopic').text data.topic - if data.topic is '' - $('#timsChatTopic').addClass 'empty' - else - $('#timsChatTopic').removeClass 'empty' - - $('.timsChatMessage').addClass 'unloaded', 800 - @handleMessages data.messages - document.title = @titleTemplate.fetch data - - # Fix smiley urls ... - $('#smilies .menu li a').each (key, value) -> - anchor = $(value) - anchor.attr 'href', anchor.attr('href').replace /.*#/, "#{target.attr('href')}#" - - - error: -> - # Reload the page to change the room the old fashion-way - # inclusive the error-message :) - window.location.reload true - beforeSend: => - return false if target.parent().hasClass('loading') or target.parent().hasClass 'activeMenuItem' - - @loading = true - target.parent().addClass 'loading' - ### - # Frees the fish - ### - freeTheFish: -> - return if $.wcfIsset 'fish' - console.warn 'Freeing the fish' - fish = $ """
#{WCF.String.escapeHTML('><((((\u00B0>')}
""" - 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()) - - fish.text '><((((\u00B0>' if left > 0 - fish.text '<\u00B0))))><' if left < 0 - - fish.animate - top: "+=#{top}" - left: "+=#{left}" - , 1e3 - , 1.5e3 - ### - # Loads new messages. - ### - getMessages: -> - $.ajax @config.messageURL, - dataType: 'json' - type: 'POST' - success: (data, textStatus, jqXHR) => - WCF.DOMNodeInsertedHandler.enable() - @handleMessages(data.messages) - @handleUsers(data.users) - WCF.DOMNodeInsertedHandler.disable() - - error: (jqXHR, textStatus, errorThrown) => - console.error 'Battle Station hit - shields at ' + (--@shields / 3 * 104) + ' percent' - if @shields is 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!' - alert 'herp i cannot load messages' - complete: => - @loading = false - beforeSend: => - return false if @loading - - @loading = true - ### - # Inserts the new messages. - # - # @param array messages - ### - handleMessages: (messages) -> - # Disable scrolling automagically when user manually scrolled - unless @oldScrollTop is null - if $('#timsChatMessageContainer').scrollTop() < @oldScrollTop - if $('#timsChatAutoscroll').data('status') is 1 - $('#timsChatAutoscroll').click() - $('#timsChatAutoscroll').addClass 'active' - $('#timsChatAutoscroll').parent().fadeOut('slow').fadeIn 'slow' - - # Insert the messages - for message in messages - continue if $.wcfIsset 'timsChatMessage' + message.messageID # Prevent problems with race condition - @events.newMessage.fire message - - output = @messageTemplate.fetch message - li = $ '
  • ' - li.attr 'id', 'timsChatMessage'+message.messageID - li.addClass 'timsChatMessage timsChatMessage'+message.type - li.addClass 'ownMessage' if message.sender is WCF.User.userID - li.append output - - li.appendTo $ '#timsChatMessageContainer > ul' - - # Autoscroll down - $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() if $('#timsChatAutoscroll').data('status') is 1 - @oldScrollTop = $('#timsChatMessageContainer').scrollTop() - ### - # Builds the userlist. - # - # @param array users - ### - handleUsers: (users) -> - foundUsers = { } - for user in users - id = 'timsChatUser-'+user.userID - element = $ '#'+id - - # Move the user to the correct position - if element[0] - 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').append element - # Insert the user - else - console.log "Inserting User: '#{user.username}'" - 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 - - a = $ '' + WCF.String.escapeHTML(user.username) + '' - a.addClass 'userLink' - a.addClass 'dropdownToggle' - a.data 'userID', user.userID - a.data 'toggle', id - - li.append a - - menu = $ '
      ' - #menu.addClass 'timsChatUserMenu' - menu.addClass 'dropdownMenu' - menu.append $ "
    • #{WCF.Language.get('chat.general.query')}
    • " - menu.append $ "
    • #{ WCF.Language.get('chat.general.kick')}
    • " - menu.append $ "
    • #{ WCF.Language.get('chat.general.ban')}
    • " - # TODO: SID and co - menu.append $ """
    • #{WCF.Language.get('chat.general.profile')}
    • """ - @events.userMenu.fire user, menu - li.append menu - - li.appendTo $ '#timsChatUserList' - - foundUsers[id] = true - - # Remove users that were not found - $('.timsChatUser').each () -> - if typeof foundUsers[$(@).attr('id')] is 'undefined' - console.log "Removing User: '#{$(@).data('username')}'" - $(@).remove(); - - - $('#toggleUsers .badge').text users.length - ### - # Initializes Server-Push - ### - initPush: () -> - unless typeof window.io is 'undefined' - console.log 'Initializing nodePush' - @socket = io.connect @config.socketIOPath - @socket.on 'connect', => - console.log 'Connected to nodePush' - @pe.getMessages.stop() - @socket.on 'disconnect', => - console.log 'Lost connection to nodePush' - @pe.getMessages = new WCF.PeriodicalExecuter $.proxy(@getMessages, @), @config.reloadTime * 1e3 - @socket.on 'newMessage', => - @getMessages() - ### - # Inserts text into our input. - # - # @param string text - # @param object options - ### - insertText: (text, options) -> - options = $.extend - append: true - submit: false - , options or {} - - text = $('#timsChatInput').val() + text if options.append - $('#timsChatInput').val text - $('#timsChatInput').keyup() - - if (options.submit) - $('#timsChatForm').submit() - else - $('#timsChatInput').focus() - ### - # Sends a notification about a message. - # - # @param object message - ### - notify: (message) -> - return if @isActive or $('#timsChatNotify').data('status') is 0 - @newMessageCount++ - - document.title = '(' + @newMessageCount + ') ' + @titleTemplate.fetch - title: $('#timsChatRoomList .activeMenuItem a').text() - - # Desktop Notifications - title = WCF.Language.get 'chat.general.notify.title' - content = message.username + message.separator + (if message.separator is ' ' then '' else ' ') + message.message - - if window.Notification? - if window.Notification.permission is 'granted' - notification = new window.Notification title, - body: content - onclick: -> - notification.close() - setTimeout notification.close, 5e3 - ### - # Refreshes the room-list. - ### - refreshRoomList: () -> - console.log 'Refreshing the roomlist' - $('#toggleRooms .ajaxLoad').show() - - $.ajax $('#toggleRooms a').data('refreshUrl'), - dataType: 'json' - type: 'POST' - success: (data, textStatus, jqXHR) => - $('#timsChatRoomList li').remove() - $('#toggleRooms .ajaxLoad').hide() - $('#toggleRooms .badge').text data.length - - for room in data - li = $ '
    • ' - li.addClass 'activeMenuItem' if room.active - $("""#{room.title}""").addClass('timsChatRoom').appendTo li - $('#timsChatRoomList ul').append li - - $('.timsChatRoom').click (event) => - return if typeof window.history.replaceState is 'undefined' - event.preventDefault() - @changeRoom $ event.target - - - console.log "Found #{data.length} rooms" - ### - # Handles submitting of messages. - # - # @param jQuery-object target - ### - submit: (target) -> - # Break if input contains only whitespace - return false if $('#timsChatInput').val().trim().length is 0 - - # Finally free the fish - @freeTheFish() if $('#timsChatInput').val().trim().toLowerCase() is '/free the fish' - - text = $('#timsChatInput').val() - - # call submit event - # TODO: Fix this - # text = @events.submit.fire text - - $('#timsChatInput').val('').focus().keyup() - proxy = 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 if not (data?.returnValues?.errorType?) - - $('#timsChatInputContainer').addClass('formError').find('.innerError').html(data.returnValues.errorType).show() - false - ### - # Toggles between user- and room-list. - # - # @param jQuery-object target - ### - toggleSidebarContents: (target) -> - return if target.parents('li').hasClass 'active' - - if target.parents('li').attr('id') is 'toggleUsers' - $('#toggleUsers').addClass 'active' - $('#toggleRooms').removeClass 'active' - - $('#timsChatRoomList').hide() - $('#timsChatUserList').show() - else if target.parents('li').attr('id') is 'toggleRooms' - $('#toggleRooms').addClass 'active' - $('#toggleUsers').removeClass 'active' - - $('#timsChatUserList').hide() - $('#timsChatRoomList').show() - ### - # Unloads the chat. - ### - unload: () -> - $.ajax @config.unloadURL, - type: 'POST' - async: false -)(jQuery, @, console) diff --git a/file/js/be.bastelstu.Chat.litcoffee b/file/js/be.bastelstu.Chat.litcoffee new file mode 100644 index 0000000..51f301d --- /dev/null +++ b/file/js/be.bastelstu.Chat.litcoffee @@ -0,0 +1,696 @@ +Main JavaScript file for Tims Chat +================================== +Copyright Information +--------------------- + + "@author Tim Düsterhus" + "@copyright 2010-2013 Tim Düsterhus" + "@license Creative Commons Attribution-NonCommercial-ShareAlike " + "@package be.bastelstu.chat" + +Setup +----- +Ensure sane values for `$` and `window` + + (($, window) -> + # Enable strict mode + "use strict"; + + # Ensure our namespace is present + window.be ?= {} + be.bastelstu ?= {} + +Overwrite `console` to add the origin in front of the message + + 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}" +be.bastelstu.Chat +================= + + be.bastelstu.Chat = Class.extend + +Attributes +---------- + +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. + + shields: 3 + +Prevents loading messages in parallel. + + loading: false + +Instances of `WCF.Template` + + titleTemplate: null + messageTemplate: null + +Attributes needed for notificationss + + newMessageCount: null + isActive: true + +Attributes needed for autocompleter + + autocompleteOffset: 0 + autocompleteValue: null + autocompleteCaret: 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() + userMenu: $.Callbacks() + submit: $.Callbacks() + +Instance of socket.io for real time chatting. + + socket: null + +Every `WCF.PeriodicalExecuter` used by the chat to allow access for 3rd party developers. + + pe: + getMessages: null + refreshRoomList: null + fish: null + +Methods +------- + +**init(@config, @titleTemplate, @messageTemplate)** +Constructor, binds needed events and initializes `@events` and `PeriodicalExecuter`s. + + init: (@config, @titleTemplate, @messageTemplate) -> + console.log 'Initializing' + +Bind events and initialize our own event system. + + @events = + newMessage: $.Callbacks() + 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! + + 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. + + autocomplete: (firstChars, offset = @autocompleteOffset) -> + +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 .activeMenuItem a').text() + @newMessageCount = 0 + @isActive = true + +Mark chat as inactive, thus enabling notifications. + + $(window).blur => + @isActive = false + +Calls the unload handler (`@unload`) before unloading the chat. + + $(window).on 'beforeunload', => + @unload() + undefined + +Inserts a smiley into the input. + + $('#smilies').on 'click', 'img', (event) => + @insertText ' ' + $(event.target).attr('alt') + ' ' + + +Switches the active sidebar tab. + + $('.timsChatSidebarTabs li').click (event) => + event.preventDefault() + @toggleSidebarContents $ event.target + + +Calls the submit handler (`@submit`) when the `#timsChatForm` is `submit`ted. + + $('#timsChatForm').submit (event) => + event.preventDefault() + @submit $ event.target + + +Autocompletes a username when TAB is pressed. + + $('#timsChatInput').keydown (event) => + if event.keyCode is 9 + event.preventDefault() + +Calculate `firstChars` to autocomplete, based on the caret position. + + @autocompleteValue = $('#timsChatInput').val() if @autocompleteValue is null + @autocompleteCaret = $('#timsChatInput').getCaret() if @autocompleteCaret is null + + beforeCaret = @autocompleteValue.substring 0, @autocompleteCaret + lastSpace = beforeCaret.lastIndexOf ' ' + beforeComplete = @autocompleteValue.substring 0, lastSpace + 1 + toComplete = @autocompleteValue.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}'" + +Insert completed value into `#timsChatInput` + + name = @autocomplete toComplete + + $('#timsChatInput').val "#{beforeComplete}#{name} #{afterComplete}" + $('#timsChatInput').setCaret (beforeComplete + name).length + 1 + @autocompleteOffset++ + +Resets autocompleter to default status, when a key is pressed that is not TAB. + + else + @autocompleteOffset = 0 + @autocompleteValue = null + @autocompleteCaret = null + +Resets autocompleter to default status, when input is `click`ed, as the position of the caret may have changed. + + $('#timsChatInput').click => + @autocompleteOffset = 0 + @autocompleteValue = null + @autocompleteCaret = null + +Refreshes the room list when the associated button is `click`ed. + + $('#timsChatRoomList button').click $.proxy @refreshRoomList, @ + +Clears the chat, by removing every single message. + + $('#timsChatClear').click (event) -> + event.preventDefault() + $('.timsChatMessage').remove() + @oldScrollTop = null + $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() + +Handling toggling when a toggable button is `click`ed. + + $('.timsChatToggle').click (event) -> + element = $ @ + icon = element.find 'span.icon' + if element.data('status') is 1 + element.data 'status', 0 + icon.removeClass('icon-circle-blank').addClass('icon-off') + element.attr 'title', element.data 'enableMessage' + else + element.data 'status', 1 + icon.removeClass('icon-off').addClass('icon-circle-blank') + element.attr 'title', element.data 'disableMessage' + + $('#timsChatInput').focus() + +Toggle fullscreen mode. + + $('#timsChatFullscreen').click (event) -> + if $(@).data 'status' + $('html').addClass 'fullscreen' + else + $('html').removeClass 'fullscreen' + +Scroll down when autoscrollis being activated. + + $('#timsChatAutoscroll').click (event) -> + $(@).removeClass 'active' + if $(@).data 'status' + $('#timsChatMessageContainer').scrollTop $('#timsChatMessageContainer ul').height() + @oldScrollTop = $('.timsChatMessageContainer').scrollTop() + +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 + +**changeRoom(target)** +Change the active chatroom. `target` is the link clicked. + + changeRoom: (target) -> + +Update URL to target URL by using `window.history.replaceState()`. + + 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' + + # Mark as active + $('.activeMenuItem .timsChatRoom').parent().removeClass 'activeMenuItem' + target.parent().addClass 'activeMenuItem' + +Update topic, hiding and showing the topic container when necessary. + + $('#timsChatTopic').text data.topic + if data.topic is '' + $('#timsChatTopic').addClass 'empty' + else + $('#timsChatTopic').removeClass 'empty' + +Mark old messages as `unloaded`. + + $('.timsChatMessage').addClass 'unloaded' + +Show the messages written before entering the room to get a quick glance at the current topic. + + @handleMessages data.messages + +Update `document.title` to reflect the cnew room. + + document.title = @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 'activeMenuItem' + + @loading = true + target.parent().addClass 'loading' + +**freeTheFish()** +Free the fish! + + freeTheFish: -> + return if $.wcfIsset 'fish' + console.warn 'Freeing the fish' + fish = $ """
      #{WCF.String.escapeHTML('><((((\u00B0>')}
      """ + 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()) + + fish.text '><((((\u00B0>' if left > 0 + fish.text '<\u00B0))))><' if left < 0 + + fish.animate + top: "+=#{top}" + left: "+=#{left}" + , 1e3 + , 1.5e3 + +**getMessages()** +Loads new messages. + + getMessages: -> + $.ajax @config.messageURL, + dataType: 'json' + type: 'POST' + +Handle reply. + + success: (data, textStatus, jqXHR) => + WCF.DOMNodeInsertedHandler.enable() + @handleMessages(data.messages) + @handleUsers(data.users) + WCF.DOMNodeInsertedHandler.disable() + +Decrease `@shields` on error and disable PeriodicalExecuters once `@shields` reaches zero. + + error: => + console.error 'Battle Station hit - shields at ' + (--@shields / 3 * 104) + ' percent' + if @shields is 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!' + alert 'herp i cannot load messages' + complete: => + @loading = false + +Prevent loading messages in parallel, as this leads to several problems. + + beforeSend: => + return false if @loading + + @loading = true + +**handleMessages(messages)** +Inserts the `messages` given into the stream. + + handleMessages: (messages) -> + +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').addClass 'active' + $('#timsChatAutoscroll').parent().fadeOut('slow').fadeIn 'slow' + +Insert the new messages. + + for message in messages + +Prevent problems with race condition + + 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.attr 'id', "timsChatMessage#{message.messageID}" + li.addClass 'timsChatMessage timsChatMessage'+message.type + li.addClass 'ownMessage' if message.sender is WCF.User.userID + li.append output + + li.appendTo $ '#timsChatMessageContainer > ul' + + +Scroll down when autoscrolling is enabled. + + $('#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. + + foundUsers = { } + +Loop all users. + + for user in users + id = "timsChatUser-#{user.userID}" + element = $ "##{id}" + +Move the user, to prevent rebuilding the entire user list. + + if element[0] + 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').append element + +Build HTML of new user and append it. + + else + console.log "Inserting User: '#{user.username}'" + 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 + + a = $ '' + WCF.String.escapeHTML(user.username) + '' + a.addClass 'userLink' + a.addClass 'dropdownToggle' + a.data 'userID', user.userID + a.data 'toggle', id + + li.append a + + menu = $ '
        ' + menu.addClass 'dropdownMenu' + menu.append $ "
      • #{WCF.Language.get('chat.general.query')}
      • " + menu.append $ "
      • #{WCF.Language.get('chat.general.kick')}
      • " + menu.append $ "
      • #{WCF.Language.get('chat.general.ban')}
      • " + # TODO: SID and co + menu.append $ """
      • #{WCF.Language.get('chat.general.profile')}
      • """ + @events.userMenu.fire user, menu + li.append menu + + li.appendTo $ '#timsChatUserList' + + 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 users.length + +**initPush()** +Initialize socket.io to enable nodePush. + + initPush: -> + if window.io? + console.log 'Initializing nodePush' + @socket = io.connect @config.socketIOPath + + @socket.on 'connect', => + console.log 'Connected to nodePush' + +Disable `@pe.getMessages` once we are connected. + + @pe.getMessages.stop() + + @socket.on 'disconnect', => + console.log 'Lost connection to nodePush' + +Reenable `@pe.getMessages` once we are disconnected. + + @pe.getMessages = new WCF.PeriodicalExecuter $.proxy(@getMessages, @), @config.reloadTime * 1e3 + + @socket.on 'newMessage', => + @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 + append: true + submit: false + , options or {} + + text = $('#timsChatInput').val() + text if options.append + $('#timsChatInput').val text + $('#timsChatInput').keyup() + + if (options.submit) + $('#timsChatForm').submit() + else + $('#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) -> + return if @isActive or $('#timsChatNotify').data('status') is 0 + @newMessageCount++ + + document.title = '(' + @newMessageCount + ') ' + @titleTemplate.fetch + title: $('#timsChatRoomList .activeMenuItem a').text() + + # Desktop Notifications + title = WCF.Language.get 'chat.general.notify.title' + content = "#{message.username}#{message.separator} #{message.message}" + + if window.Notification? + if window.Notification.permission is 'granted' + notification = new window.Notification title, + body: content + onclick: -> + notification.close() + setTimeout notification.close, 5e3 + +**refreshRoomList()** +Updates the room list. + + refreshRoomList: -> + console.log 'Refreshing the roomlist' + $('#toggleRooms .ajaxLoad').show() + + $.ajax $('#toggleRooms a').data('refreshUrl'), + dataType: 'json' + type: 'POST' + success: (data, textStatus, jqXHR) => + $('#timsChatRoomList li').remove() + $('#toggleRooms .ajaxLoad').hide() + $('#toggleRooms .badge').text data.length + + for room in data + li = $ '
      • ' + li.addClass 'activeMenuItem' if room.active + $("""#{room.title}""").addClass('timsChatRoom').appendTo li + $('#timsChatRoomList ul').append li + +Bind click event for inline room change if we have the history API available. + + if window.history?.replaceState? + $('.timsChatRoom').click (event) => + event.preventDefault() + @changeRoom $ event.target + + console.log "Found #{data.length} rooms" + +**submit(target)** +Submits the message. + + submit: (target) -> + # Break if input contains only whitespace + return false if $('#timsChatInput').val().trim().length is 0 + + # Free the fish! + @freeTheFish() if $('#timsChatInput').val().trim().toLowerCase() is '/free the fish' + + text = $('#timsChatInput').val() + + # call submit event + # TODO: Fix this + # text = @events.submit.fire text + + $('#timsChatInput').val('').focus().keyup() + + proxy = 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 if not (data?.returnValues?.errorType?) + + $('#timsChatInputContainer').addClass('formError').find('.innerError').html(data.returnValues.errorType).show() + false + +**toggleSidebarContents(target)** +Switches the active sidebar tab to the one belonging to `target`. + + toggleSidebarContents: (target) -> + return if target.parents('li').hasClass 'active' + + if target.parents('li').attr('id') is 'toggleUsers' + $('#toggleUsers').addClass 'active' + $('#toggleRooms').removeClass 'active' + + $('#timsChatRoomList').hide() + $('#timsChatUserList').show() + else if target.parents('li').attr('id') is 'toggleRooms' + $('#toggleRooms').addClass 'active' + $('#toggleUsers').removeClass 'active' + + $('#timsChatUserList').hide() + $('#timsChatRoomList').show() + +**unload()** +Sends leave notification to the server. + + unload: -> + $.ajax @config.unloadURL, + type: 'POST' + async: false + )(jQuery, @) diff --git a/file/lib/data/message/Message.class.php b/file/lib/data/message/Message.class.php index dea41bd..216c186 100644 --- a/file/lib/data/message/Message.class.php +++ b/file/lib/data/message/Message.class.php @@ -126,7 +126,7 @@ public function jsonify($raw = false) { $separator = ':'; break; default: - $separator = ' '; + $separator = ''; break; }