mirror of
https://github.com/wbbaddons/Tims-Chat.git
synced 2024-12-22 21:40:08 +00:00
7468aa342e
Closes #66
1048 lines
34 KiB
Plaintext
1048 lines
34 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 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}"
|
|
|
|
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
|
|
scrollUpNotifications = off
|
|
chatSession = Date.now()
|
|
userList =
|
|
current: {}
|
|
allTime: {}
|
|
currentRoom = {}
|
|
fileUploaded = no
|
|
|
|
errorVisible = false
|
|
inputErrorHidingTimer = null
|
|
|
|
lastMessage = null
|
|
openChannel = 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
|
|
|
|
v =
|
|
titleTemplate: null
|
|
messageTemplate: null
|
|
userTemplate: null
|
|
config: null
|
|
|
|
Initialize **Tims Chat**. Bind needed DOM events and initialize data structures.
|
|
|
|
initialized = false
|
|
init = (roomID, 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 currentRoom
|
|
|
|
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', ->
|
|
return undefined if errorVisible
|
|
|
|
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 private channel menu
|
|
|
|
$('#privateChannelsMenu').on 'click', '.privateChannel', -> openPrivateChannel $(@).data 'privateChannelID'
|
|
|
|
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) ->
|
|
do event.preventDefault
|
|
|
|
text = do $('#timsChatInput').val().trim
|
|
$('#timsChatInput').val('').focus().change()
|
|
|
|
return false if text.length is 0
|
|
|
|
text = "/whisper #{userList.allTime[openChannel].username}, #{text}" unless openChannel is 0
|
|
|
|
# Free the fish!
|
|
do 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: ->
|
|
do hideInputError
|
|
|
|
do getMessages
|
|
failure: (data) ->
|
|
return true unless (data?.returnValues?.errorType?) or (data?.message?)
|
|
|
|
showInputError (data?.returnValues?.errorType) ? data.message
|
|
|
|
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
|
|
do event.preventDefault
|
|
input = $ @
|
|
|
|
autocomplete.value ?= do input.val
|
|
autocomplete.caret ?= do 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}'"
|
|
|
|
if beforeComplete is '' and (toComplete.substring 0, 1) is '/'
|
|
regex = new RegExp "^#{WCF.String.escapeRegExp toComplete.substring 1}", "i"
|
|
commands = (command for command in v.config.installedCommands when regex.test command)
|
|
|
|
toComplete = '/' + commands[autocomplete.offset++ % commands.length] + ' ' if commands.length isnt 0
|
|
else
|
|
regex = new RegExp "^#{WCF.String.escapeRegExp toComplete}", "i"
|
|
|
|
users = [ ]
|
|
for userID, user of userList.current
|
|
users.push user.username if regex.test user.username
|
|
|
|
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
|
|
do $('#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 -> do refreshRoomList
|
|
|
|
Clear the chat by removing every single message once the clear button is `clicked`.
|
|
|
|
$('#timsChatClear').click (event) ->
|
|
do event.preventDefault
|
|
do $('.timsChatMessageContainer.active .timsChatMessage').remove
|
|
$('.timsChatMessageContainer.active').scrollTop $('.timsChatMessageContainer.active').prop 'scrollHeight'
|
|
|
|
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'
|
|
|
|
do $('#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) ->
|
|
# Force dropdowns to reorientate
|
|
$('.dropdownMenu').data 'orientationX', ''
|
|
|
|
if $(@).data 'status'
|
|
$('html').addClass 'fullscreen'
|
|
else
|
|
$('html').removeClass 'fullscreen'
|
|
|
|
Toggle checkboxes.
|
|
|
|
$('#timsChatMark').click (event) ->
|
|
if $(@).data 'status'
|
|
$('.timsChatMessageContainer').addClass 'markEnabled'
|
|
else
|
|
$('.timsChatMessageContainer').removeClass 'markEnabled'
|
|
|
|
Hide topic container.
|
|
|
|
$('.jsTopicCloser').on 'click', ->
|
|
if $('.timsChatMessageContainer.active').data('userID') is 0
|
|
$('#timsChatTopic').addClass 'hidden'
|
|
else
|
|
closePrivateChannel $('.timsChatMessageContainer.active').data('userID')
|
|
|
|
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.active').scrollTop $('.timsChatMessageContainer.active').prop 'scrollHeight'
|
|
|
|
$('.timsChatMessageContainer.active').on 'scroll', (event) ->
|
|
element = $ @
|
|
scrollTop = element.scrollTop()
|
|
scrollHeight = element.prop 'scrollHeight'
|
|
height = element.height()
|
|
|
|
if scrollTop < scrollHeight - height - 25
|
|
if $('#timsChatAutoscroll').data('status') is 1
|
|
scrollUpNotifications = on
|
|
do $('#timsChatAutoscroll').click
|
|
|
|
if scrollTop > scrollHeight - height - 10
|
|
if $('#timsChatAutoscroll').data('status') is 0
|
|
scrollUpNotifications = off
|
|
$(@).removeClass 'notification'
|
|
do $('#timsChatAutoscroll').click
|
|
|
|
Enable duplicate tab detection.
|
|
|
|
try
|
|
window.localStorage.setItem 'be.bastelstu.chat.session', chatSession
|
|
$(window).on 'storage', (event) ->
|
|
if event.originalEvent.key is 'be.bastelstu.chat.session'
|
|
showError WCF.Language.get 'chat.error.duplicateTab' unless parseInt(event.originalEvent.newValue) is chatSession
|
|
|
|
Ask for permissions to use Desktop notifications when notifications are activated.
|
|
|
|
if window.Notification?
|
|
$('#timsChatNotify').click (event) ->
|
|
return unless $(@).data 'status'
|
|
unless window.Notification.permission is 'granted'
|
|
window.Notification.requestPermission (permission) ->
|
|
window.Notification.permission ?= permission
|
|
|
|
events.newMessage.add notify
|
|
|
|
Initialize the `PeriodicalExecuter`s
|
|
|
|
pe.refreshRoomList = new WCF.PeriodicalExecuter refreshRoomList, 60e3
|
|
pe.getMessages = new WCF.PeriodicalExecuter getMessages, v.config.reloadTime * 1e3
|
|
|
|
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'
|
|
do pe.getMessages.stop
|
|
|
|
be.bastelstu.wcf.nodePush.onDisconnect ->
|
|
console.log 'Enabling periodic loading'
|
|
do 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 and join the chat.
|
|
|
|
join roomID
|
|
do $('#timsChatInput').enable().jCounter().focus
|
|
|
|
console.log "Finished initializing"
|
|
|
|
true
|
|
|
|
Shows an error message below the input.
|
|
|
|
showInputError = (message) ->
|
|
$('#timsChatInputContainer').addClass('formError').find('.innerError').show().html message
|
|
|
|
clearTimeout inputErrorHidingTimer if inputErrorHidingTimer?
|
|
inputErrorHidingTimer = setTimeout ->
|
|
do hideInputError
|
|
, 5e3
|
|
|
|
Hides the error message below the input.
|
|
|
|
hideInputError = ->
|
|
clearTimeout inputErrorHidingTimer if inputErrorHidingTimer?
|
|
inputErrorHidingTimer = null
|
|
|
|
do $('#timsChatInputContainer').removeClass('formError').find('.innerError').hide
|
|
|
|
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: 'fixed'
|
|
top: '50%'
|
|
left: '50%'
|
|
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) < ($(window).width() - fish.width())
|
|
top *= -1 unless fish.height() < (fish.position().top + top) < ($(window).height() - fish.height())
|
|
|
|
if left > 0
|
|
fish.text '><((((\u00B0>' if left > 0
|
|
else if left < 0
|
|
fish.text '<\u00B0))))><'
|
|
|
|
fish.animate
|
|
top: (fish.position().top + top)
|
|
left: (fish.position().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
|
|
handleMessages data.messages
|
|
handleUsers data.users
|
|
WCF.DOMNodeInsertedHandler.execute()
|
|
error: ->
|
|
console.error "Message loading failed, #{--remainingFailures} remaining"
|
|
if remainingFailures <= 0
|
|
do freeTheFish
|
|
console.error 'To many failures, aborting'
|
|
|
|
showError WCF.Language.get 'chat.error.onMessageLoad'
|
|
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.active').trigger 'scroll'
|
|
|
|
for message in messages
|
|
message.isInPrivateChannel = (String(message.type) is v.config.messageTypes.WHISPER) and ($.wcfIsset("timsChatMessageContainer#{message.receiver}") or $.wcfIsset("timsChatMessageContainer#{message.sender}"))
|
|
|
|
events.newMessage.fire message
|
|
|
|
createNewMessage = yes
|
|
if $('.timsChatMessage:last-child .text').is('ul') and lastMessage isnt null and lastMessage.type in [ 0, 7 ]
|
|
if lastMessage.type is message.type and lastMessage.sender is message.sender and lastMessage.receiver is message.receiver and lastMessage.isInPrivateChannel is message.isInPrivateChannel
|
|
createNewMessage = no
|
|
|
|
if createNewMessage
|
|
message.isFollowUp = no
|
|
output = v.messageTemplate.fetch
|
|
message: message
|
|
messageTypes: v.config.messageTypes
|
|
|
|
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
|
|
|
|
if message.isInPrivateChannel and message.sender is WCF.User.userID
|
|
li.appendTo $ "#timsChatMessageContainer#{message.receiver} > ul"
|
|
else if message.isInPrivateChannel
|
|
li.appendTo $ "#timsChatMessageContainer#{message.sender} > ul"
|
|
else
|
|
li.appendTo $ '#timsChatMessageContainer0 > ul'
|
|
else
|
|
message.isFollowUp = yes
|
|
output = v.messageTemplate.fetch
|
|
message: message
|
|
messageTypes: v.config.messageTypes
|
|
|
|
if message.isInPrivateChannel and message.sender is WCF.User.userID
|
|
messageContainerID = message.receiver
|
|
else if message.isInPrivateChannel
|
|
messageContainerID = message.sender
|
|
else
|
|
messageContainerID = 0
|
|
|
|
$("#timsChatMessageContainer#{messageContainerID} .timsChatMessage:last-child .text").append $(output).find('.text li:last-child')
|
|
|
|
lastMessage = message
|
|
$('.timsChatMessageContainer.active').scrollTop $('.timsChatMessageContainer.active').prop('scrollHeight') if $('#timsChatAutoscroll').data('status') is 1
|
|
|
|
Rebuild the userlist based on the given `users`.
|
|
|
|
handleUsers = (users) ->
|
|
foundUsers = { }
|
|
userList.current = { }
|
|
|
|
for user in users
|
|
do (user) ->
|
|
userList.current[user.userID] = userList.allTime[user.userID] = user
|
|
|
|
id = "timsChatUser#{user.userID}"
|
|
|
|
Move the user to the new position if he was found in the old list.
|
|
|
|
if $.wcfIsset id
|
|
console.log "Moving User: '#{user.username}'"
|
|
element = $("##{id}").detach()
|
|
|
|
if user.awayStatus?
|
|
element.addClass 'away'
|
|
element.attr 'title', user.awayStatus
|
|
else
|
|
element.removeClass 'away'
|
|
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 'you' if user.userID is WCF.User.userID
|
|
li.addClass 'suspended' if user.suspended
|
|
if user.awayStatus?
|
|
li.addClass 'away'
|
|
li.attr 'title', user.awayStatus
|
|
li.data 'username', user.username
|
|
|
|
li.append v.userTemplate.fetch user
|
|
|
|
menu = $ '<ul></ul>'
|
|
unless user.userID is WCF.User.userID
|
|
menu.append $("<li><a>#{WCF.Language.get('chat.general.query')}</a></li>").click -> openPrivateChannel user.userID
|
|
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
|
|
|
|
if menu.find('li').length
|
|
li.append menu
|
|
menu.addClass 'dropdownMenu'
|
|
li.addClass 'dropdown'
|
|
|
|
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')}'"
|
|
WCF.Dropdown.removeDropdown $(@).attr 'id'
|
|
do $(@).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
|
|
do $('#timsChatInput').keyup
|
|
|
|
if options.submit
|
|
do $('#timsChatForm').submit
|
|
else
|
|
do $('#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) ->
|
|
if scrollUpNotifications
|
|
$('.timsChatMessageContainer.active').addClass 'notification'
|
|
|
|
if message.isInPrivateChannel
|
|
if message.sender is WCF.User.userID
|
|
privateChannelID = message.receiver
|
|
else
|
|
privateChannelID = message.sender
|
|
|
|
if $('.timsChatMessageContainer.active').data('userID') isnt privateChannelID
|
|
$("#privateChannel#{privateChannelID}").addClass 'notify'
|
|
else if $('.timsChatMessageContainer.active').data('userID') isnt 0
|
|
$("#privateChannel0").addClass 'notify'
|
|
|
|
return if isActive or $('#timsChatNotify').data('status') is 0
|
|
|
|
document.title = v.titleTemplate.fetch $.extend {}, currentRoom,
|
|
newMessageCount: ++newMessageCount
|
|
|
|
title = WCF.Language.get 'chat.general.notify.title'
|
|
content = "#{message.username}#{message.separator} #{message.message}"
|
|
|
|
if window.Notification?.permission is 'granted'
|
|
do ->
|
|
notification = new window.Notification title,
|
|
body: content
|
|
onclick: ->
|
|
do notification.close
|
|
setTimeout ->
|
|
do notification.close
|
|
, 5e3
|
|
|
|
Fetch the roomlist from the server and update it in the GUI.
|
|
|
|
refreshRoomList = ->
|
|
console.log 'Refreshing the roomlist'
|
|
|
|
new WCF.Action.Proxy
|
|
autoSend: true
|
|
data:
|
|
actionName: 'getRoomList'
|
|
className: 'chat\\data\\room\\RoomAction'
|
|
showLoadingOverlay: false
|
|
suppressErrors: true
|
|
success: (data) ->
|
|
do $('.timsChatRoom').remove
|
|
$('#toggleRooms .badge').text data.returnValues.length
|
|
|
|
for room in data.returnValues
|
|
li = $ '<li></li>'
|
|
li.addClass 'active' if room.active
|
|
$("""<a href="#{room.link}">#{WCF.String.escapeHTML(room.title)} <span class="badge">#{WCF.String.formatNumeric room.userCount}</span></a>""").addClass('timsChatRoom').data('roomID', room.roomID).appendTo li
|
|
$('#timsChatRoomList ul').append li
|
|
|
|
if window.history?.replaceState?
|
|
$('.timsChatRoom').click (event) ->
|
|
do event.preventDefault
|
|
target = $ @
|
|
|
|
window.history.replaceState {}, '', target.attr 'href'
|
|
|
|
join target.data 'roomID'
|
|
$('#timsChatRoomList .active').removeClass 'active'
|
|
target.parent().addClass 'active'
|
|
|
|
console.log "Found #{data.returnValues.length} rooms"
|
|
|
|
Shows an unrecoverable error with the given text.
|
|
|
|
showError = (text) ->
|
|
return if errorVisible
|
|
errorVisible = true
|
|
|
|
loading = true
|
|
|
|
do pe.refreshRoomList.stop
|
|
do pe.getMessages.stop
|
|
|
|
errorDialog = $("""
|
|
<div id="timsChatLoadingErrorDialog">
|
|
<p>#{text}</p>
|
|
</div>
|
|
""").appendTo 'body'
|
|
|
|
formSubmit = $("""<div class="formSubmit"></div>""").appendTo errorDialog
|
|
|
|
reloadButton = $("""<button class="buttonPrimary">#{WCF.Language.get 'chat.error.reload'}</button>""").appendTo formSubmit
|
|
reloadButton.on 'click', -> do window.location.reload
|
|
|
|
$('#timsChatLoadingErrorDialog').wcfDialog
|
|
closable: false
|
|
title: WCF.Language.get 'wcf.global.error.title'
|
|
|
|
Joins a room.
|
|
|
|
join = (roomID) ->
|
|
loading = true
|
|
new WCF.Action.Proxy
|
|
autoSend: true
|
|
data:
|
|
actionName: 'join'
|
|
className: 'chat\\data\\room\\RoomAction'
|
|
parameters:
|
|
roomID: roomID
|
|
success: (data) ->
|
|
loading = false
|
|
|
|
$('#timsChatTopic').removeClass 'hidden'
|
|
currentRoom = data.returnValues
|
|
currentRoom.roomID = roomID
|
|
|
|
$('#timsChatTopic > .topic').text currentRoom.topic
|
|
if currentRoom.topic.trim() is ''
|
|
$('#timsChatTopic').addClass 'empty'
|
|
else
|
|
$('#timsChatTopic').removeClass 'empty'
|
|
|
|
$('.timsChatMessage').addClass 'unloaded'
|
|
|
|
document.title = v.titleTemplate.fetch currentRoom
|
|
handleMessages currentRoom.messages
|
|
do getMessages
|
|
do refreshRoomList
|
|
failure: (data) ->
|
|
showError WCF.Language.get 'chat.error.join', data
|
|
|
|
Open private channel
|
|
|
|
openPrivateChannel = (userID) ->
|
|
userID = parseInt userID
|
|
|
|
console.log "Opening private channel #{userID}"
|
|
|
|
unless $.wcfIsset "timsChatMessageContainer#{userID}"
|
|
return unless userList.allTime[userID]?
|
|
|
|
div = $ '<div>'
|
|
div.attr 'id', "timsChatMessageContainer#{userID}"
|
|
div.data 'userID', userID
|
|
div.addClass 'timsChatMessageContainer'
|
|
div.addClass 'marginTop'
|
|
div.addClass 'container'
|
|
div.wrapInner '<ul>'
|
|
$('#timsChatMessageContainer0').after div
|
|
|
|
$('.privateChannel').removeClass 'active'
|
|
|
|
if userID isnt 0
|
|
$('#timsChatTopic').removeClass 'hidden empty'
|
|
$('#timsChatTopic > .topic').html WCF.Language.get 'chat.general.privateChannelTopic', {username: userList.allTime[userID].username}
|
|
$('#timsChatTopic > .jsTopicCloser').attr 'title', WCF.Language.get 'chat.general.closePrivateChannel'
|
|
|
|
unless $.wcfIsset "privateChannel#{userID}"
|
|
li = $ '<li>'
|
|
li.attr 'id', "privateChannel#{userID}"
|
|
li.data 'privateChannelID', userID
|
|
li.addClass 'privateChannel'
|
|
|
|
span = $ '<span class="userAvatar framed" />'
|
|
|
|
avatar = $ userList.allTime[userID].avatar[16]
|
|
avatar.addClass 'jsTooltip'
|
|
avatar.attr 'title', userList.allTime[userID].username
|
|
avatar.wrap span
|
|
li.append avatar.parent().addClass 'small'
|
|
|
|
avatar = $ userList.allTime[userID].avatar[32]
|
|
avatar.addClass 'jsTooltip'
|
|
avatar.attr 'title', userList.allTime[userID].username
|
|
avatar.wrap span
|
|
li.append avatar.parent().addClass 'large'
|
|
|
|
$('#privateChannelsMenu ul').append li
|
|
|
|
$('#privateChannelsMenu').addClass 'shown'
|
|
else
|
|
$('#timsChatTopic > .topic').text currentRoom.topic
|
|
$('#timsChatTopic > .jsTopicCloser').attr 'title', WCF.Language.get 'chat.general.closeTopic'
|
|
if currentRoom.topic.trim() is ''
|
|
$('#timsChatTopic').addClass 'empty'
|
|
else
|
|
$('#timsChatTopic').removeClass 'empty'
|
|
|
|
do WCF.DOMNodeInsertedHandler.execute
|
|
|
|
$('.timsChatMessageContainer').removeClass 'active'
|
|
$("#timsChatMessageContainer#{userID}").addClass 'active'
|
|
$("#privateChannel#{userID}").addClass('active').removeClass 'notify'
|
|
openChannel = userID
|
|
|
|
Close private channel
|
|
|
|
closePrivateChannel = (userID) ->
|
|
unless userID is 0
|
|
do $("#privateChannel#{userID}").remove
|
|
do $("#timsChatMessageContainer#{userID}").remove
|
|
|
|
if $('#privateChannelsMenu li').length <= 1
|
|
$('#privateChannelsMenu').removeClass 'shown'
|
|
|
|
openPrivateChannel 0
|
|
|
|
Bind the given callback to the given event.
|
|
|
|
addListener = (event, callback) ->
|
|
return false unless events[event]?
|
|
events[event].add callback
|
|
|
|
true
|
|
|
|
Remove the given callback from the given event.
|
|
|
|
removeListener = (event, callback) ->
|
|
return false unless events[event]?
|
|
events[event].remove callback
|
|
|
|
true
|
|
|
|
The following code handles attachment uploads
|
|
|
|
Enable attachment code if `WCF.Attachment.Upload` is defined
|
|
|
|
if WCF?.Attachment?.Upload? and $('#timsChatUploadContainer').length
|
|
Attachment = WCF.Attachment.Upload.extend
|
|
fileUploaded: no
|
|
|
|
Initialize WCF.Attachment.Upload
|
|
See WCF.Attachment.Upload.init()
|
|
|
|
init: ->
|
|
@_super $('#timsChatUploadContainer'), $(false), 'be.bastelstu.chat.message', 0, 0, 0, 1, null
|
|
unless @_supportsAJAXUpload
|
|
$('#timsChatUploadDropdownMenu .uploadButton').click => do @_showOverlay
|
|
|
|
label = $ '#timsChatUploadDropdownMenu li > span > label'
|
|
parent = do label.parent
|
|
|
|
css = parent.css ['padding-top', 'padding-right', 'padding-bottom', 'padding-left']
|
|
|
|
label.css css
|
|
label.css 'margin', "-#{css['padding-top']} -#{css['padding-right']} -#{css['padding-bottom']} -#{css['padding-left']}"
|
|
$('#timsChatUpload').click ->
|
|
$('#timsChatUpload > span.icon-ban-circle').removeClass('icon-ban-circle').addClass 'icon-paper-clip'
|
|
do $('#timsChatUploadContainer .innerError').remove
|
|
|
|
Overwrite WCF.Attachment.Upload._createButton() to create the upload button as small button into a button group
|
|
|
|
_createButton: ->
|
|
if @_supportsAJAXUpload
|
|
@_fileUpload = $ """<input id="timsChatUploadInput" type="file" name="#{@_name}" />"""
|
|
@_fileUpload.change => do @_upload
|
|
@_fileUpload.appendTo 'body'
|
|
|
|
_removeButton: ->
|
|
do @_fileUpload.remove
|
|
|
|
See WCF.Attachment.Upload._getParameters()
|
|
|
|
_getParameters: ->
|
|
@_tmpHash = do Math.random
|
|
@_parentObjectID = currentRoom.roomID
|
|
|
|
do @_super
|
|
|
|
|
|
_upload: ->
|
|
files = @_fileUpload.prop 'files'
|
|
if files.length
|
|
$('#timsChatUpload > span.icon').removeClass('icon-paper-clip icon-ban-circle').addClass('icon-spinner')
|
|
do @_super
|
|
|
|
Create a message containing the uploaded attachment
|
|
|
|
_insert: (event) ->
|
|
objectID = $(event.currentTarget).data 'objectID'
|
|
|
|
new WCF.Action.Proxy
|
|
autoSend: true
|
|
data:
|
|
actionName: 'sendAttachment'
|
|
className: 'chat\\data\\message\\MessageAction'
|
|
parameters:
|
|
objectID: objectID
|
|
tmpHash: @_tmpHash
|
|
parentObjectID: 1#@_parentObjectID
|
|
showLoadingOverlay: false
|
|
|
|
success: ->
|
|
do $('#timsChatUploadDropdownMenu .jsDeleteButton').parent().remove
|
|
do $('#timsChatUploadDropdownMenu .sendAttachmentButton').remove
|
|
do $('#timsChatUploadDropdownMenu .uploadButton').show
|
|
$('#timsChatUpload > span.icon').removeClass('icon-ok-sign').addClass 'icon-paper-clip'
|
|
fileUploaded = no
|
|
|
|
failure: (data) ->
|
|
false
|
|
|
|
_initFile: (file) ->
|
|
li = $("""<li class="uploadProgress">
|
|
<span>
|
|
<progress max="100"></progress>
|
|
</span>
|
|
</li>"""
|
|
).data('filename', file.name)
|
|
|
|
$('#timsChatUploadDropdownMenu').append li
|
|
do $('#timsChatUploadDropdownMenu .uploadButton').hide
|
|
# validate file size
|
|
if @_buttonSelector.data('maxSize') < file.size
|
|
# remove progress bar
|
|
do li.find('progress').remove
|
|
|
|
# upload icon
|
|
$('#timsChatUpload > span.icon-spinner').removeClass('icon-spinner').addClass 'icon-ban-circle'
|
|
|
|
# error message
|
|
$('#timsChatUpload').addClass('uploadFailed').after """<small class="innerError">#{WCF.Language.get('wcf.attachment.upload.error.tooLarge')}</small>"""
|
|
|
|
do @_error
|
|
li.addClass 'uploadFailed'
|
|
li
|
|
|
|
_validateLimit: ->
|
|
innerError = @_buttonSelector.next 'small.innerError'
|
|
|
|
if fileUploaded
|
|
# reached limit
|
|
unless innerError.length
|
|
innerError = $('<small class="innerError" />').insertAfter '#timsChatUpload'
|
|
|
|
innerError.html WCF.Language.get('wcf.attachment.upload.error.reachedLimit')
|
|
innerError.css 'position', 'absolute'
|
|
|
|
return false
|
|
|
|
# remove previous errors
|
|
do innerError.remove
|
|
|
|
true
|
|
|
|
_success: (uploadID, data) ->
|
|
for li in @_uploadMatrix[uploadID]
|
|
do li.find('progress').remove
|
|
li.removeClass('uploadProgress').addClass 'sendAttachmentButton'
|
|
|
|
li.find('span').addClass('box32').append """
|
|
<div class="framed attachmentImageContainer">
|
|
<span class="attachmentTinyThumbnail icon icon32 icon-paper-clip"></span>
|
|
</div>
|
|
<div class="containerHeaderline">
|
|
<p></p>
|
|
<small></small>
|
|
<p>#{WCF.Language.get('wcf.global.button.submit')}</p>
|
|
</div>"""
|
|
|
|
li.click (event) => @_insert(event)
|
|
|
|
filename = li.data 'filename'
|
|
internalFileID = li.data 'internalFileID'
|
|
|
|
if data.returnValues and data.returnValues.attachments[internalFileID]
|
|
if data.returnValues.attachments[internalFileID].tinyURL
|
|
li.find('.box32 > div.attachmentImageContainer > .icon-paper-clip').replaceWith $("""<img src="#{data.returnValues.attachments[internalFileID].tinyURL}'" alt="" class="attachmentTinyThumbnail" style="width: 32px; height: 32px;" />""")
|
|
|
|
link = $ '<a href="" class="jsTooltip"></a>'
|
|
link.attr {'href': data.returnValues.attachments[internalFileID].url, 'title': filename}
|
|
|
|
unless parseInt(data.returnValues.attachments[internalFileID].isImage) is 0
|
|
link.addClass('jsImageViewer')
|
|
|
|
if !data.returnValues.attachments[internalFileID].tinyURL
|
|
li.find('.box32 > div.attachmentImageContainer > .icon-paper-clip').replaceWith $("""<img src="#{data.returnValues.attachments[internalFileID].url}'" alt="" class="attachmentTinyThumbnail" style="width: 32px; height: 32px;" />""")
|
|
|
|
li.find('.attachmentTinyThumbnail').wrap link
|
|
li.find('small').append data.returnValues.attachments[internalFileID].formattedFilesize
|
|
|
|
li.data 'objectID', data.returnValues.attachments[internalFileID].attachmentID
|
|
|
|
deleteButton = $ """
|
|
<li>
|
|
<span class="jsDeleteButton" data-object-id="#{data.returnValues.attachments[internalFileID].attachmentID}" data-confirm-message="#{WCF.Language.get('wcf.attachment.delete.sure')}">
|
|
<span class="icon icon16 icon-remove pointer jsTooltip" />
|
|
<span>#{WCF.Language.get('wcf.global.button.delete')}</span>
|
|
</span>
|
|
</li>"""
|
|
li.parent().append deleteButton
|
|
fileUploaded = yes
|
|
else
|
|
$('#timsChatUpload .icon-spinner').removeClass('icon-spinner').addClass 'icon-ban-circle'
|
|
|
|
if data.returnValues and data.returnValues.errors[internalFileID]
|
|
errorMessage = data.returnValues.errors[internalFileID].errorType
|
|
else
|
|
errorMessage = 'uploadFailed'
|
|
|
|
$('#timsChatUpload').addClass('uploadFailed').after """<small class="innerError">#{WCF.Language.get('wcf.attachment.upload.error.' + errorMessage)}</small>"""
|
|
do $('#timsChatUploadDropdownMenu .sendAttachmentButton').remove
|
|
do $('#timsChatUploadDropdownMenu .uploadButton').show
|
|
fileUploaded = no
|
|
|
|
do WCF.DOMNodeInsertedHandler.execute
|
|
|
|
$('#timsChatUpload > span.icon').removeClass('icon-spinner').addClass 'icon-ok-sign'
|
|
do $('#timsChatUploadDropdownMenu .uploadProgress').remove
|
|
do $('#timsChatUploadDropdownMenu .sendAttachmentButton').show
|
|
|
|
_error: (jqXHR, textStatus, errorThrown) ->
|
|
$('#timsChatUpload > .icon-spinner').removeClass('icon-spinner').addClass 'icon-ban-circle'
|
|
unless $('#timsChatUpload').hasClass('uploadFailed')
|
|
$('#timsChatUpload').addClass('uploadFailed').after """<small class="innerError">#{WCF.Language.get('wcf.attachment.upload.error.uploadFailed')}</small>"""
|
|
|
|
do $('#timsChatUploadDropdownMenu .uploadProgress').remove
|
|
do $('#timsChatUploadDropdownMenu .uploadButton').show
|
|
fileUploaded = no
|
|
|
|
Action = {}
|
|
Action.Delete = WCF.Action.Delete.extend
|
|
triggerEffect: (objectIDs) ->
|
|
for index in @_containers
|
|
container = $ "##{index}"
|
|
if WCF.inArray container.find(@_buttonSelector).data('objectID'), objectIDs
|
|
self = @
|
|
container.wcfBlindOut 'up', (event) ->
|
|
parent = do $(@).parent
|
|
do $(@).remove
|
|
do parent.find('.sendAttachmentButton').remove
|
|
do parent.find('.uploadButton').show
|
|
$('#timsChatUpload > .icon-ok-sign').removeClass('icon-ok-sign').addClass 'icon-paper-clip'
|
|
|
|
self._containers.splice(self._containers.indexOf $(@).wcfIdentify(), 1)
|
|
self._didTriggerEffect($ @)
|
|
fileUploaded = no
|
|
return
|
|
And finally export the public methods and variables.
|
|
|
|
Chat =
|
|
init: init
|
|
getMessages: getMessages
|
|
refreshRoomList: refreshRoomList
|
|
insertText: insertText
|
|
freeTheFish: freeTheFish
|
|
join: join
|
|
listener:
|
|
add: addListener
|
|
remove: removeListener
|
|
Chat.Attachment = Attachment if Attachment?
|
|
Chat.Action = Action if Attachment?
|
|
|
|
window.be ?= {}
|
|
be.bastelstu ?= {}
|
|
window.be.bastelstu.Chat = Chat
|
|
)(jQuery, @)
|