Merge branch 'develop' of github.com:martini/zammad into develop

This commit is contained in:
Felix Niklas 2015-11-26 10:29:33 +01:00
commit 20abe7ff87
20 changed files with 610 additions and 323 deletions

View file

@ -443,25 +443,11 @@ class App.Controller extends Spine.Controller
} }
processData: true, processData: true,
success: (data, status, xhr) -> success: (data, status, xhr) ->
App.SessionStorage.set( "user-ticket-popover::#{params.user_id}", data )
# load assets
App.Collection.loadAssets( data.assets ) App.Collection.loadAssets( data.assets )
show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } ) show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } )
) )
# get data # get data
data = App.SessionStorage.get( "user-ticket-popover::#{params.user_id}" )
if data
show( params, { open: data.ticket_ids_open, closed: data.ticket_ids_closed } )
@delay(
->
fetch(params)
1000
'fetch'
)
else
fetch(params) fetch(params)
userTicketPopupsDestroy: => userTicketPopupsDestroy: =>

View file

@ -1,9 +1,5 @@
class App.ChannelChat extends App.Controller class App.ChannelChat extends App.Controller
events: events:
'click .js-add': 'new'
'click .js-edit': 'edit'
'click .js-remove': 'remove'
'click .js-widget': 'widget'
'change .js-params': 'updateParams' 'change .js-params': 'updateParams'
'input .js-params': 'updateParams' 'input .js-params': 'updateParams'
'submit .js-demo-head': 'onUrlSubmit' 'submit .js-demo-head': 'onUrlSubmit'
@ -11,6 +7,7 @@ class App.ChannelChat extends App.Controller
'click .js-selectBrowserWidth': 'selectBrowserWidth' 'click .js-selectBrowserWidth': 'selectBrowserWidth'
'click .js-swatch': 'usePaletteColor' 'click .js-swatch': 'usePaletteColor'
'click .js-toggle-chat': 'toggleChat' 'click .js-toggle-chat': 'toggleChat'
'click .js-chatSetting': 'toggleChatSetting'
elements: elements:
'.js-browser': 'browser' '.js-browser': 'browser'
@ -27,13 +24,14 @@ class App.ChannelChat extends App.Controller
'.js-code': 'code' '.js-code': 'code'
'.js-palette': 'palette' '.js-palette': 'palette'
'.js-color': 'colorField' '.js-color': 'colorField'
'.js-chatSetting': 'chatSetting'
apiOptions: [ apiOptions: [
{ {
name: 'channel' name: 'chatId'
default: "'default'" default: '1'
type: 'String' type: 'Number'
description: 'Name of the chat-channel.' description: 'Identifier of the chat-topic.'
} }
{ {
name: 'show' name: 'show'
@ -54,18 +52,18 @@ class App.ChannelChat extends App.Controller
description: "If left empty, the host gets auto-detected - in this case %s. The auto-detection reads out the host from the <script> tag. If you don't include it via a <script> tag you need to specify the host." description: "If left empty, the host gets auto-detected - in this case %s. The auto-detection reads out the host from the <script> tag. If you don't include it via a <script> tag you need to specify the host."
descriptionSubstitute: window.location.origin descriptionSubstitute: window.location.origin
} }
{
name: 'port'
default: 6042
type: 'Int'
description: ''
}
{ {
name: 'debug' name: 'debug'
default: false default: false
type: 'Boolean' type: 'Boolean'
description: 'Enables console logging.' description: 'Enables console logging.'
} }
{
name: 'title'
default: "'<strong>Chat</strong> with us!'"
type: 'String'
description: 'Welcome Title shown on the closed chat. Can contain HTML.'
}
{ {
name: 'fontSize' name: 'fontSize'
default: 'undefined' default: 'undefined'
@ -75,7 +73,7 @@ class App.ChannelChat extends App.Controller
{ {
name: 'flat' name: 'flat'
default: 'false' default: 'false'
type: 'boolean' type: 'Boolean'
description: 'Removes the shadows for a flat look.' description: 'Removes the shadows for a flat look.'
} }
{ {
@ -91,18 +89,29 @@ class App.ChannelChat extends App.Controller
description: 'This class gets added to the button on initialization and gets removed once the chat connection got established.' description: 'This class gets added to the button on initialization and gets removed once the chat connection got established.'
} }
{ {
name: 'title' name: 'cssAutoload'
default: "'<strong>Chat</strong> with us!'" default: 'true'
type: 'Boolean'
description: 'Automatically loads the chat.css file. If you want to use your own css, just set it to false.'
}
{
name: 'cssUrl'
default: 'undefined'
type: 'String' type: 'String'
description: 'Welcome Title shown on the closed chat. Can contain HTML.' description: 'Location of an external chat.css file.'
} }
] ]
isOpen: true isOpen: true
browserWidth: 1280 browserWidth: 1280
previewUrl: ''
constructor: -> constructor: ->
super super
@title 'Chat'
if @Session.get('email')
@previewUrl = "www.#{@Session.get('email').replace(/^.+?\@/, '')}"
@load() @load()
@widgetDesignerPermanentParams = @widgetDesignerPermanentParams =
@ -124,15 +133,15 @@ class App.ChannelChat extends App.Controller
) )
render: (data = {}) => render: (data = {}) =>
chats = []
for chat_id in data.chat_ids
chats.push App.Chat.find(chat_id)
@html App.view('channel/chat')( @html App.view('channel/chat')(
baseurl: window.location.origin baseurl: window.location.origin
chats: chats
apiOptions: @apiOptions apiOptions: @apiOptions
previewUrl: @previewUrl
chatSetting: @Config.get('chat')
)
new Topics(
el: @$('.js-topics')
) )
@code.each (i, block) -> @code.each (i, block) ->
@ -236,7 +245,7 @@ class App.ChannelChat extends App.Controller
palette = _.map palette, tinycolor palette = _.map palette, tinycolor
# filter white # filter white
palette = _.filter palette, (color) => palette = _.filter palette, (color) ->
color.getLuminance() < 0.85 color.getLuminance() < 0.85
htmlString = '' htmlString = ''
@ -264,47 +273,12 @@ class App.ChannelChat extends App.Controller
@isOpen = @chat.hasClass('is-open') @isOpen = @chat.hasClass('is-open')
@updatePreview() @updatePreview()
new: (e) => toggleChatSetting: =>
new App.ControllerGenericNew( value = @chatSetting.prop('checked')
pageData: setting = App.Setting.findByAttribute('name', 'chat')
title: 'Chats' setting.state_current = { value: value }
object: 'Chat' setting.save()
objects: 'Chats' @Config.set('chat', value)
genericObject: 'Chat'
callback: @load
container: @el.closest('.content')
large: true
)
edit: (e) =>
e.preventDefault()
id = $(e.target).closest('tr').data('id')
new App.ControllerGenericEdit(
id: id
genericObject: 'Chat'
pageData:
object: 'Chat'
container: @el.closest('.content')
callback: @load
)
remove: (e) =>
e.preventDefault()
id = $(e.target).closest('tr').data('id')
item = App.Chat.find(id)
new App.ControllerGenericDestroyConfirm(
item: item
container: @el.closest('.content')
callback: @load
)
widget: (e) ->
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new Widget(
permanent:
id: id
)
updateParams: => updateParams: =>
quote = (value) -> quote = (value) ->
@ -347,4 +321,53 @@ class App.ChannelChat extends App.Controller
@paramsBlock.each (i, block) -> @paramsBlock.each (i, block) ->
hljs.highlightBlock block hljs.highlightBlock block
App.Config.set( 'Chat Widget', { prio: 4000, name: 'Chat Widget', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'Chat', { prio: 4000, name: 'Chat', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, role: ['Admin'] }, 'NavBarAdmin' )
class Topics extends App.Controller
events:
'click .js-add': 'new'
'click .js-edit': 'edit'
'click .js-remove': 'remove'
constructor: ->
super
@render()
render: =>
@html App.view('channel/topics')(
chats: App.Chat.all()
)
new: (e) =>
new App.ControllerGenericNew(
pageData:
title: 'Chats'
object: 'Chat'
objects: 'Chats'
genericObject: 'Chat'
callback: @render
container: @el.closest('.content')
large: true
)
edit: (e) =>
e.preventDefault()
id = $(e.target).closest('tr').data('id')
new App.ControllerGenericEdit(
id: id
genericObject: 'Chat'
pageData:
object: 'Chat'
container: @el.closest('.content')
callback: @render
)
remove: (e) =>
e.preventDefault()
id = $(e.target).closest('tr').data('id')
item = App.Chat.find(id)
new App.ControllerGenericDestroyConfirm(
item: item
container: @el.closest('.content')
callback: @render
)

View file

@ -3,22 +3,30 @@ class App.ChannelForm extends App.Controller
events: events:
'change form.js-params': 'updateParams' 'change form.js-params': 'updateParams'
'keyup form.js-params': 'updateParams' 'keyup form.js-params': 'updateParams'
'click .js-formSetting': 'toggleFormSetting'
elements:
'.js-paramsBlock': 'paramsBlock'
'.js-formSetting': 'formSetting'
constructor: -> constructor: ->
super super
@title 'Form' @title 'Form'
@render() @subscribeId = App.Setting.subscribe(@render, initFetch: true)
@updateParams()
new App.SettingsArea(
el: @$('.js-settings')
area: 'Form::Base'
)
render: -> render: =>
App.Setting.unsubscribe(@subscribeId)
setting = App.Setting.findByAttribute('name', 'form_ticket_create')
@html App.view('channel/form')( @html App.view('channel/form')(
baseurl: window.location.origin baseurl: window.location.origin
formSetting: setting.state_current.value
) )
@paramsBlock.each (i, block) ->
hljs.highlightBlock block
@updateParams()
updateParams: -> updateParams: ->
quote = (string) -> quote = (string) ->
string = string.replace('\'', '\\\'') string = string.replace('\'', '\\\'')
@ -36,4 +44,10 @@ class App.ChannelForm extends App.Controller
paramString += " #{key}: '#{quote(value)}'" paramString += " #{key}: '#{quote(value)}'"
@$('.js-modal-params').html(paramString) @$('.js-modal-params').html(paramString)
toggleFormSetting: =>
value = @formSetting.prop('checked')
setting = App.Setting.findByAttribute('name', 'form_ticket_create')
setting.state_current = { value: value }
setting.save()
App.Config.set( 'Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set( 'Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, role: ['Admin'] }, 'NavBarAdmin' )

View file

@ -13,27 +13,17 @@ class App.WidgetTag extends App.Controller
constructor: -> constructor: ->
super super
@cacheKey = "tags::#{@object_type}::#{@object.id}" @key = "tags::#{@object_type}::#{@object.id}"
if @tags if @tags
@render() @render()
return return
@tags = App.SessionStorage.get( @cacheKey ) || []
if !_.isEmpty(@tags)
@render()
@delay(
=>
@fetch()
1000
'fetch'
)
else
@fetch() @fetch()
fetch: => fetch: =>
@ajax( @ajax(
id: @cacheKey id: @key
type: 'GET' type: 'GET'
url: @apiPath + '/tags' url: @apiPath + '/tags'
data: data:
@ -42,7 +32,6 @@ class App.WidgetTag extends App.Controller
processData: true processData: true
success: (data, status, xhr) => success: (data, status, xhr) =>
@tags = data.tags @tags = data.tags
App.SessionStorage.set( @cacheKey, @tags )
@render() @render()
) )

View file

@ -6,44 +6,17 @@
<div class="page-content"> <div class="page-content">
<p><%- @T('You can create chat widgets for your webpages to allow visitors to chat with you.') %></p> <p><%- @T('You can create chat widgets for your webpages to allow visitors to chat with you.') %></p>
<h2><%- @T('Enable') %>/<%- @T('Disable') %></h2>
<form>
<div class="zammad-switch">
<input name="chat" type="checkbox" id="setting-chat" class="js-chatSetting" <% if @chatSetting: %>checked<% end %>>
<label for="setting-chat"></label>
</div>
</form>
<h2><%- @T('Topics') %></h2> <h2><%- @T('Topics') %></h2>
<p><%- @T('You can create multiple chat topics.') %></p> <p><%- @T('You can create multiple chat topics.') %></p>
<table class="settings-list"> <div class="js-topics"></div>
<thead>
<tr>
<th style="white-space: nowrap;"><%- @T('Name') %></th>
<th style="white-space: nowrap;"><%- @T('Note') %></th>
<th style="white-space: nowrap;"><%- @T('Max. clients in waitlist') %></th>
<th style="white-space: nowrap;"><%- @T('Delete') %></th>
</tr>
</thead>
<tbody>
<% for chat in @chats: %>
<tr data-id="<%= chat.id %>">
<td>
<label class="inline-label">
<a class="js-edit is-clickable"><%= chat.name %></a>
</label>
<td>
<label class="inline-label">
<%= chat.note %>
</label>
<td>
<label class="inline-label">
<%= chat.max_queue %>
</label>
<td>
<div class="settings-list-rowControls">
<div class="btn btn--text js-remove">
<%- @Icon('trash') %> <%- @T('Remove') %>
</div>
</div>
<% end %>
<tr>
<td colspan="4" class="settings-list-action-cell js-add">
<%- @Icon('plus-small') %> <%- @T('Add') %>
</tbody>
</table>
<h2><%- @T('Designer') %></h2> <h2><%- @T('Designer') %></h2>
@ -62,7 +35,7 @@
<div class="browser chat-demo js-browser"> <div class="browser chat-demo js-browser">
<form class="browser-head js-demo-head" novalidate> <form class="browser-head js-demo-head" novalidate>
<div class="browser-input"> <div class="browser-input">
<input type="url" class="js-testurl-input" id="preview-iframe" placeholder="zammad.org"> <input type="url" class="js-testurl-input" id="preview-iframe" value="<%= @previewUrl %>" placeholder="www.zammad.org">
<div class="loading icon small muted"></div> <div class="loading icon small muted"></div>
</div> </div>
<input type="submit" class="btn" value="<%- @T('Load') %>"> <input type="submit" class="btn" value="<%- @T('Load') %>">
@ -119,7 +92,7 @@
<label for="form-chat-title"><%- @T('Chat Title') %></label> <label for="form-chat-title"><%- @T('Chat Title') %></label>
</div> </div>
<div class="controls"> <div class="controls">
<input class="js-chatTitle" type="text" id="form-chat-title" name="title" value="&lt;strong&gt;Chat&lt;/strong&gt; with us!"> <input class="js-chatTitle" type="text" id="form-chat-title" name="title" placeholder="&lt;strong&gt;Chat&lt;/strong&gt; with us!">
</div> </div>
<span class="help-block"><%- @T('Shown when the chat is closed.') %></span> <span class="help-block"><%- @T('Shown when the chat is closed.') %></span>
</div> </div>

View file

@ -6,15 +6,19 @@
<div class="page-content"> <div class="page-content">
<p><%- @T('With form you can add a formular to your web page witch directly generates a Ticket for you.') %></p> <p><%- @T('With form you can add a formular to your web page witch directly generates a Ticket for you.') %></p>
<div class="js-settings"></div> <h2><%- @T('Enable') %>/<%- @T('Disable') %></h2>
<form>
<div class="zammad-switch">
<input name="form_ticket_create" type="checkbox" id="setting-form" class="js-formSetting" <% if @formSetting: %>checked<% end %>>
<label for="setting-form"></label>
</div>
</form>
<hr> <h2><%- @T('Designer') %></h2>
<h2><%- @T('Widget Designer') %></h2>
<form class="js-params"> <form class="js-params">
<fieldset> <fieldset>
<div class="input form-group"> <div class="input form-group formGroup--halfSize">
<div class="formGroup-label"> <div class="formGroup-label">
<label for="form-message-title"><%- @T('Title of the form') %></label> <label for="form-message-title"><%- @T('Title of the form') %></label>
</div> </div>
@ -22,7 +26,7 @@
<input type="text" id="form-message-title" name="messageTitle" value="<%- @Ti('Feedback Form') %>"> <input type="text" id="form-message-title" name="messageTitle" value="<%- @Ti('Feedback Form') %>">
</div> </div>
</div> </div>
<div class="input form-group"> <div class="input form-group formGroup--halfSize">
<div class="formGroup-label"> <div class="formGroup-label">
<label for="form-message-submit"><%- @T('Name of form submit button') %></label> <label for="form-message-submit"><%- @T('Name of form submit button') %></label>
</div> </div>
@ -95,8 +99,7 @@
<p><%- @T('You need to add the following Java Script code snipped to your web page') %>: <p><%- @T('You need to add the following Java Script code snipped to your web page') %>:
<pre> <pre><code class="language-html js-paramsBlock">&lt;button id="feedback-form"&gt;Feedback&lt;/button&gt;
&lt;button id="feedback-form"&gt;Feedback&lt;/button&gt;
&lt;script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"&gt;&lt;/script&gt; &lt;script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"&gt;&lt;/script&gt;
@ -106,5 +109,5 @@ $(function() {
<span class="js-modal-params"></span> <span class="js-modal-params"></span>
}); });
}); });
&lt;/script&gt;</pre> &lt;/script&gt;</code></pre>
</div> </div>

View file

@ -0,0 +1,41 @@
<table class="settings-list">
<thead>
<tr>
<th style="white-space: nowrap;"><%- @T('chatId') %></th>
<th style="white-space: nowrap;"><%- @T('Name') %></th>
<th style="white-space: nowrap;"><%- @T('Note') %></th>
<th style="white-space: nowrap;"><%- @T('Max. clients in waitlist') %></th>
<th style="white-space: nowrap;"><%- @T('Delete') %></th>
</tr>
</thead>
<tbody>
<% for chat in @chats: %>
<tr data-id="<%= chat.id %>">
<td>
<label class="inline-label">
<%= chat.id %>
</label>
<td>
<label class="inline-label">
<a class="js-edit is-clickable"><%= chat.name %></a>
</label>
<td>
<label class="inline-label">
<%= chat.note %>
</label>
<td>
<label class="inline-label">
<%= chat.max_queue %>
</label>
<td>
<div class="settings-list-rowControls">
<div class="btn btn--text js-remove">
<%- @Icon('trash') %> <%- @T('Remove') %>
</div>
</div>
<% end %>
<tr>
<td colspan="5" class="settings-list-action-cell js-add">
<%- @Icon('plus-small') %> <%- @T('Add') %>
</tbody>
</table>

View file

@ -11,6 +11,8 @@ class ChatsController < ApplicationController
chat_ids.push chat.id chat_ids.push chat.id
assets = chat.assets(assets) assets = chat.assets(assets)
} }
setting = Setting.find_by(name: 'chat')
assets = setting.assets(assets)
render json: { render json: {
chat_ids: chat_ids, chat_ids: chat_ids,
assets: assets, assets: assets,

View file

@ -134,9 +134,10 @@ class Channel::EmailParser
if !data[:body].valid_encoding? if !data[:body].valid_encoding?
data[:body] = data[:body].encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?') data[:body] = data[:body].encode('utf-8', 'binary', invalid: :replace, undef: :replace, replace: '?')
end end
end
# html attachment/body may exists and will be converted to text # html attachment/body may exists and will be converted to text
else if !mail.text_part || !data[:body] || data[:body] == ''
filename = '-no name-' filename = '-no name-'
if mail.html_part && mail.html_part.body if mail.html_part && mail.html_part.body
filename = 'message.html' filename = 'message.html'

View file

@ -14,10 +14,15 @@ do($ = window.jQuery, window) ->
host: '' host: ''
debug: false debug: false
flat: false flat: false
lang: undefined
cssAutoload: true
cssUrl: undefined
fontSize: undefined fontSize: undefined
buttonClass: 'open-zammad-chat' buttonClass: 'open-zammad-chat'
inactiveClass: 'is-inactive' inactiveClass: 'is-inactive'
title: '<strong>Chat</strong> with us!' title: '<strong>Chat</strong> with us!'
idleTimeout: 8
inactiveTimeout: 20
_messageCount: 0 _messageCount: 0
isOpen: true isOpen: true
@ -31,7 +36,10 @@ do($ = window.jQuery, window) ->
state: 'offline' state: 'offline'
initialQueueDelay: 10000 initialQueueDelay: 10000
wsReconnectEnable: true wsReconnectEnable: true
strings: translations:
de:
'<strong>Chat</strong> with us!': '<strong>Chat</strong> mit uns!'
'Online': 'Online'
'Online': 'Online' 'Online': 'Online'
'Offline': 'Offline' 'Offline': 'Offline'
'Connecting': 'Verbinden' 'Connecting': 'Verbinden'
@ -42,19 +50,23 @@ do($ = window.jQuery, window) ->
'All colleges are busy.': 'Alle Kollegen sind belegt.' 'All colleges are busy.': 'Alle Kollegen sind belegt.'
'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.' 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.'
'Start new conversation': 'Neue Konversation starten' 'Start new conversation': 'Neue Konversation starten'
'Since you didn\'t respond in the last %s your conversation with <strong>%s</strong> got closed.': 'Da sie in den letzten %s nichts geschrieben haben wurde ihre Konversation mit <strong>%s</strong> geschlossen.' 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.'
'minutes': 'Minuten' 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.'
sessionId: undefined sessionId: undefined
T: (string, items...) => T: (string, items...) =>
if !@strings[string] if @options.lang && @options.lang isnt 'en'
if !@translations[@options.lang]
@log 'notice', "Translation '#{@options.lang}' needed!"
else
translations = @translations[@options.lang]
if !translations[string]
@log 'notice', "Translation needed for '#{string}'" @log 'notice', "Translation needed for '#{string}'"
translation = @strings[string] || string string = translations[string] || string
if items if items
for item in items for item in items
translation = translation.replace(/%s/, item) string = string.replace(/%s/, item)
string
translation
log: (level, string...) => log: (level, string...) =>
return if !@options.debug && level is 'debug' return if !@options.debug && level is 'debug'
@ -76,16 +88,26 @@ do($ = window.jQuery, window) ->
@options = $.extend {}, @defaults, options @options = $.extend {}, @defaults, options
# check prerequisites # check prerequisites
if !$
@state = 'unsupported'
@log 'notice', 'Chat: no jquery found!'
return
if !window.WebSocket or !sessionStorage if !window.WebSocket or !sessionStorage
@state = 'unsupported' @state = 'unsupported'
@log 'notice', 'Chat: Browser not supported!' @log 'notice', 'Chat: Browser not supported!'
return return
if !@options.chatId if !@options.chatId
@state = 'unsupported' @state = 'unsupported'
@log 'error', 'Chat: need chatId as option!' @log 'error', 'Chat: need chatId as option!'
return return
# detect language
if !@options.lang
@options.lang = $('html').attr('lang')
if @options.lang
@options.lang = @options.lang.replace(/-.+?$/, '') # replace "-xx" of xx-xx
@log 'debug', "lang: #{@options.lang}"
@el = $(@view('chat')( @el = $(@view('chat')(
title: @options.title title: @options.title
)) ))
@ -105,6 +127,8 @@ do($ = window.jQuery, window) ->
@wsConnect() @wsConnect()
@loadCss()
checkForEnter: (event) => checkForEnter: (event) =>
if not event.shiftKey and event.keyCode is 13 if not event.shiftKey and event.keyCode is 13
event.preventDefault() event.preventDefault()
@ -125,7 +149,9 @@ do($ = window.jQuery, window) ->
@log 'debug', 'ws:onmessage', pipe @log 'debug', 'ws:onmessage', pipe
switch pipe.event switch pipe.event
when 'chat_error' when 'chat_error'
@log 'error', pipe.data @log 'notice', pipe.data
if pipe.data && pipe.data.state is 'chat_disabled'
@wsClose()
when 'chat_session_message' when 'chat_session_message'
return if pipe.data.self_written return if pipe.data.self_written
@receiveMessage pipe.data @receiveMessage pipe.data
@ -176,6 +202,8 @@ do($ = window.jQuery, window) ->
$(".#{ @options.buttonClass }").hide() $(".#{ @options.buttonClass }").hide()
reopenSession: (data) => reopenSession: (data) =>
@inactiveTimeoutStart()
unfinishedMessage = sessionStorage.getItem 'unfinished_message' unfinishedMessage = sessionStorage.getItem 'unfinished_message'
# rerender chat history # rerender chat history
@ -218,6 +246,7 @@ do($ = window.jQuery, window) ->
@isTyping = new Date() @isTyping = new Date()
@send 'chat_session_typing', @send 'chat_session_typing',
session_id: @sessionId session_id: @sessionId
@inactiveTimeoutStart()
onSubmit: (event) => onSubmit: (event) =>
event.preventDefault() event.preventDefault()
@ -225,9 +254,10 @@ do($ = window.jQuery, window) ->
sendMessage: -> sendMessage: ->
message = @input.val() message = @input.val()
return if !message return if !message
@inactiveTimeoutStart()
sessionStorage.removeItem 'unfinished_message' sessionStorage.removeItem 'unfinished_message'
messageElement = @view('message') messageElement = @view('message')
@ -255,6 +285,8 @@ do($ = window.jQuery, window) ->
session_id: @sessionId session_id: @sessionId
receiveMessage: (data) => receiveMessage: (data) =>
@inactiveTimeoutStart()
# hide writing indicator # hide writing indicator
@onAgentTypingEnd() @onAgentTypingEnd()
@ -289,10 +321,10 @@ do($ = window.jQuery, window) ->
@isOpen = true @isOpen = true
if !@sessionId if !@sessionId
@session_init() @sessionInit()
onOpenAnimationEnd: -> onOpenAnimationEnd: =>
#@showTimeout() @idleTimeoutStop()
close: (event) => close: (event) =>
return @state if @state is 'off' or @state is 'unsupported' return @state if @state is 'off' or @state is 'unsupported'
@ -301,12 +333,25 @@ do($ = window.jQuery, window) ->
# only close if session_id exists # only close if session_id exists
return if !@sessionId return if !@sessionId
# send close
@send 'chat_session_close',
session_id: @sessionId
# stop timer
@inactiveTimeoutStop()
# delete input store
sessionStorage.removeItem 'unfinished_message'
# stop delay of initial queue position # stop delay of initial queue position
if @onInitialQueueDelayId if @onInitialQueueDelayId
clearTimeout(@onInitialQueueDelayId) clearTimeout(@onInitialQueueDelayId)
if event
@closeWindow() @closeWindow()
@setSessionId undefined
closeWindow: => closeWindow: =>
@el.removeClass('zammad-chat-is-open') @el.removeClass('zammad-chat-is-open')
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight() remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
@ -317,12 +362,6 @@ do($ = window.jQuery, window) ->
@disconnect() @disconnect()
@isOpen = false @isOpen = false
@send 'chat_session_close',
session_id: @sessionId
@setSessionId undefined
sessionStorage.removeItem 'unfinished_message'
# restart connection # restart connection
@onWebSocketOpen() @onWebSocketOpen()
@ -430,7 +469,7 @@ do($ = window.jQuery, window) ->
scrollToBottom: -> scrollToBottom: ->
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')) @el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
session_init: -> sessionInit: ->
@send('chat_session_init') @send('chat_session_init')
detectHost: -> detectHost: ->
@ -466,6 +505,7 @@ do($ = window.jQuery, window) ->
@reconnectDelayId = setTimeout(@wsConnect, 5000) @reconnectDelayId = setTimeout(@wsConnect, 5000)
onWebSocketOpen: => onWebSocketOpen: =>
@idleTimeoutStart()
@sessionId = sessionStorage.getItem('sessionId') @sessionId = sessionStorage.getItem('sessionId')
@log 'debug', 'ws connected' @log 'debug', 'ws connected'
@ -493,6 +533,7 @@ do($ = window.jQuery, window) ->
@addStatus @T('Chat closed by %s', data.realname) @addStatus @T('Chat closed by %s', data.realname)
@disableInput() @disableInput()
@setAgentOnlineState 'offline' @setAgentOnlineState 'offline'
@inactiveTimeoutStop()
disconnect: -> disconnect: ->
@showLoader() @showLoader()
@ -534,8 +575,11 @@ do($ = window.jQuery, window) ->
showTimeout: -> showTimeout: ->
@el.find('.zammad-chat-body').html @view('timeout') @el.find('.zammad-chat-body').html @view('timeout')
agent: @agent.name agent: @agent.name
delay: 10 delay: @options.inactiveTimeout
unit: @T('minutes') @close()
reload = ->
location.reload()
@el.find('.js-restart').click reload
showLoader: -> showLoader: ->
@el.find('.zammad-chat-body').html @view('loader')() @el.find('.zammad-chat-body').html @view('loader')()
@ -548,4 +592,48 @@ do($ = window.jQuery, window) ->
.attr('data-status', state) .attr('data-status', state)
.text @T(capitalizedState) .text @T(capitalizedState)
loadCss: ->
return if !@options.cssAutoload
url = @options.cssUrl
if !url
url = @options.host
.replace(/^wss/i, 'https')
.replace(/^ws/i, 'http')
.replace(/\/ws/i, '')
url += '/assets/chat/chat.css'
@log 'debug', "load css from '#{url}'"
styles = "@import url('#{url}');"
newSS = document.createElement('link')
newSS.rel = 'stylesheet'
newSS.href = 'data:text/css,' + escape(styles)
document.getElementsByTagName('head')[0].appendChild(newSS)
inactiveTimeoutStart: =>
@inactiveTimeoutStop()
delay = =>
@log 'debug', "Inactive timeout of #{@options.inactiveTimeout} minutes, show timeout screen."
@state = 'off'
@setAgentOnlineState 'offline'
@showTimeout()
@wsClose()
@inactiveTimeoutStopDelayId = setTimeout(delay, @options.inactiveTimeout * 1000 * 60)
inactiveTimeoutStop: =>
return if !@inactiveTimeoutStopDelayId
clearTimeout(@inactiveTimeoutStopDelayId)
idleTimeoutStart: =>
@idleTimeoutStop()
delay = =>
@log 'debug', "Idle timeout of #{@options.idleTimeout} minutes, hide widget"
@state = 'off'
@hide()
@wsClose()
@idleTimeoutStopDelayId = setTimeout(delay, @options.idleTimeout * 1000 * 60)
idleTimeoutStop: =>
return if !@idleTimeoutStopDelayId
clearTimeout(@idleTimeoutStopDelayId)
window.ZammadChat = ZammadChat window.ZammadChat = ZammadChat

View file

@ -105,7 +105,7 @@
margin: 0 1em; margin: 0 1em;
display: inline-block; display: inline-block;
line-height: 2em; line-height: 2em;
padding: 0 0.7em; padding: 0 .7em;
border-radius: 1em; border-radius: 1em;
background: rgba(0, 0, 0, 0.1); background: rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; } box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; }
@ -296,6 +296,7 @@
align-items: flex-start; align-items: flex-start;
border-top: 1px solid #ededed; border-top: 1px solid #ededed;
padding: 0; padding: 0;
margin: 0;
line-height: 1.4em; line-height: 1.4em;
box-shadow: 0 1px rgba(0, 0, 0, 0.01), 0 -1px rgba(0, 0, 0, 0.02); box-shadow: 0 1px rgba(0, 0, 0, 0.01), 0 -1px rgba(0, 0, 0, 0.02);
position: relative; position: relative;
@ -318,6 +319,7 @@
box-shadow: none; box-shadow: none;
padding: 1em 2em; padding: 1em 2em;
outline: none; outline: none;
box-shadow: none;
resize: none; resize: none;
-webkit-flex: 1; -webkit-flex: 1;
-ms-flex: 1; -ms-flex: 1;
@ -356,9 +358,7 @@
display: block; } display: block; }
/* /*
# Flat Design # Flat Design
*/ */
.zammad-chat--flat .zammad-chat-header, .zammad-chat--flat .zammad-chat-header,
.zammad-chat--flat .zammad-chat-body { .zammad-chat--flat .zammad-chat-body {

View file

@ -14,10 +14,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
host: '', host: '',
debug: false, debug: false,
flat: false, flat: false,
lang: void 0,
cssAutoload: true,
cssUrl: void 0,
fontSize: void 0, fontSize: void 0,
buttonClass: 'open-zammad-chat', buttonClass: 'open-zammad-chat',
inactiveClass: 'is-inactive', inactiveClass: 'is-inactive',
title: '<strong>Chat</strong> with us!' title: '<strong>Chat</strong> with us!',
idleTimeout: 8,
inactiveTimeout: 20
}; };
ZammadChat.prototype._messageCount = 0; ZammadChat.prototype._messageCount = 0;
@ -44,7 +49,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.wsReconnectEnable = true; ZammadChat.prototype.wsReconnectEnable = true;
ZammadChat.prototype.strings = { ZammadChat.prototype.translations = {
de: {
'<strong>Chat</strong> with us!': '<strong>Chat</strong> mit uns!',
'Online': 'Online',
'Online': 'Online', 'Online': 'Online',
'Offline': 'Offline', 'Offline': 'Offline',
'Connecting': 'Verbinden', 'Connecting': 'Verbinden',
@ -55,26 +63,34 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
'All colleges are busy.': 'Alle Kollegen sind belegt.', 'All colleges are busy.': 'Alle Kollegen sind belegt.',
'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.', 'You are on waiting list position <strong>%s</strong>.': 'Sie sind in der Warteliste an der Position <strong>%s</strong>.',
'Start new conversation': 'Neue Konversation starten', 'Start new conversation': 'Neue Konversation starten',
'Since you didn\'t respond in the last %s your conversation with <strong>%s</strong> got closed.': 'Da sie in den letzten %s nichts geschrieben haben wurde ihre Konversation mit <strong>%s</strong> geschlossen.', 'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.',
'minutes': 'Minuten' 'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.'
}
}; };
ZammadChat.prototype.sessionId = void 0; ZammadChat.prototype.sessionId = void 0;
ZammadChat.prototype.T = function() { ZammadChat.prototype.T = function() {
var i, item, items, len, string, translation; var i, item, items, len, string, translations;
string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : []; string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : [];
if (!this.strings[string]) { if (this.options.lang && this.options.lang !== 'en') {
if (!this.translations[this.options.lang]) {
this.log('notice', "Translation '" + this.options.lang + "' needed!");
} else {
translations = this.translations[this.options.lang];
if (!translations[string]) {
this.log('notice', "Translation needed for '" + string + "'"); this.log('notice', "Translation needed for '" + string + "'");
} }
translation = this.strings[string] || string; string = translations[string] || string;
}
}
if (items) { if (items) {
for (i = 0, len = items.length; i < len; i++) { for (i = 0, len = items.length; i < len; i++) {
item = items[i]; item = items[i];
translation = translation.replace(/%s/, item); string = string.replace(/%s/, item);
} }
} }
return translation; return string;
}; };
ZammadChat.prototype.log = function() { ZammadChat.prototype.log = function() {
@ -103,6 +119,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
function ZammadChat(options) { function ZammadChat(options) {
this.idleTimeoutStop = bind(this.idleTimeoutStop, this);
this.idleTimeoutStart = bind(this.idleTimeoutStart, this);
this.inactiveTimeoutStop = bind(this.inactiveTimeoutStop, this);
this.inactiveTimeoutStart = bind(this.inactiveTimeoutStart, this);
this.setAgentOnlineState = bind(this.setAgentOnlineState, this); this.setAgentOnlineState = bind(this.setAgentOnlineState, this);
this.onConnectionEstablished = bind(this.onConnectionEstablished, this); this.onConnectionEstablished = bind(this.onConnectionEstablished, this);
this.setSessionId = bind(this.setSessionId, this); this.setSessionId = bind(this.setSessionId, this);
@ -119,11 +139,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this); this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this);
this.closeWindow = bind(this.closeWindow, this); this.closeWindow = bind(this.closeWindow, this);
this.close = bind(this.close, this); this.close = bind(this.close, this);
this.onOpenAnimationEnd = bind(this.onOpenAnimationEnd, this);
this.open = bind(this.open, this); this.open = bind(this.open, this);
this.renderMessage = bind(this.renderMessage, this); this.renderMessage = bind(this.renderMessage, this);
this.receiveMessage = bind(this.receiveMessage, this); this.receiveMessage = bind(this.receiveMessage, this);
this.onSubmit = bind(this.onSubmit, this); this.onSubmit = bind(this.onSubmit, this);
this.onTypingEnd = bind(this.onTypingEnd, this);
this.onInput = bind(this.onInput, this); this.onInput = bind(this.onInput, this);
this.reopenSession = bind(this.reopenSession, this); this.reopenSession = bind(this.reopenSession, this);
this.onError = bind(this.onError, this); this.onError = bind(this.onError, this);
@ -135,6 +155,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.log = bind(this.log, this); this.log = bind(this.log, this);
this.T = bind(this.T, this); this.T = bind(this.T, this);
this.options = $.extend({}, this.defaults, options); this.options = $.extend({}, this.defaults, options);
if (!$) {
this.state = 'unsupported';
this.log('notice', 'Chat: no jquery found!');
return;
}
if (!window.WebSocket || !sessionStorage) { if (!window.WebSocket || !sessionStorage) {
this.state = 'unsupported'; this.state = 'unsupported';
this.log('notice', 'Chat: Browser not supported!'); this.log('notice', 'Chat: Browser not supported!');
@ -145,6 +170,13 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.log('error', 'Chat: need chatId as option!'); this.log('error', 'Chat: need chatId as option!');
return; return;
} }
if (!this.options.lang) {
this.options.lang = $('html').attr('lang');
}
if (this.options.lang) {
this.options.lang = this.options.lang.replace(/-.+?$/, '');
this.log('debug', "lang: " + this.options.lang);
}
this.el = $(this.view('chat')({ this.el = $(this.view('chat')({
title: this.options.title title: this.options.title
})); }));
@ -159,6 +191,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
input: this.onInput input: this.onInput
}); });
this.wsConnect(); this.wsConnect();
this.loadCss();
} }
ZammadChat.prototype.checkForEnter = function(event) { ZammadChat.prototype.checkForEnter = function(event) {
@ -190,7 +223,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.log('debug', 'ws:onmessage', pipe); this.log('debug', 'ws:onmessage', pipe);
switch (pipe.event) { switch (pipe.event) {
case 'chat_error': case 'chat_error':
this.log('error', pipe.data); this.log('notice', pipe.data);
if (pipe.data && pipe.data.state === 'chat_disabled') {
this.wsClose();
}
break; break;
case 'chat_session_message': case 'chat_session_message':
if (pipe.data.self_written) { if (pipe.data.self_written) {
@ -263,6 +299,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.reopenSession = function(data) { ZammadChat.prototype.reopenSession = function(data) {
var i, len, message, ref, unfinishedMessage; var i, len, message, ref, unfinishedMessage;
this.inactiveTimeoutStart();
unfinishedMessage = sessionStorage.getItem('unfinished_message'); unfinishedMessage = sessionStorage.getItem('unfinished_message');
if (data.agent) { if (data.agent) {
this.onConnectionEstablished(data); this.onConnectionEstablished(data);
@ -293,24 +330,18 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.onInput = function() { ZammadChat.prototype.onInput = function() {
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread'); this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
sessionStorage.setItem('unfinished_message', this.input.val()); sessionStorage.setItem('unfinished_message', this.input.val());
return this.onTypingStart(); return this.onTyping();
}; };
ZammadChat.prototype.onTypingStart = function() { ZammadChat.prototype.onTyping = function() {
if (this.isTypingTimeout) { if (this.isTyping && this.isTyping > new Date(new Date().getTime() - 1500)) {
clearTimeout(this.isTypingTimeout); return;
} }
this.isTypingTimeout = setTimeout(this.onTypingEnd, 1500); this.isTyping = new Date();
if (!this.isTyping) { this.send('chat_session_typing', {
this.isTyping = true;
return this.send('chat_session_typing', {
session_id: this.sessionId session_id: this.sessionId
}); });
} return this.inactiveTimeoutStart();
};
ZammadChat.prototype.onTypingEnd = function() {
return this.isTyping = false;
}; };
ZammadChat.prototype.onSubmit = function(event) { ZammadChat.prototype.onSubmit = function(event) {
@ -324,6 +355,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
if (!message) { if (!message) {
return; return;
} }
this.inactiveTimeoutStart();
sessionStorage.removeItem('unfinished_message'); sessionStorage.removeItem('unfinished_message');
messageElement = this.view('message')({ messageElement = this.view('message')({
message: message, message: message,
@ -340,7 +372,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
} }
this.input.val(''); this.input.val('');
this.scrollToBottom(); this.scrollToBottom();
this.isTyping = false;
return this.send('chat_session_message', { return this.send('chat_session_message', {
content: message, content: message,
id: this._messageCount, id: this._messageCount,
@ -349,6 +380,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.receiveMessage = function(data) { ZammadChat.prototype.receiveMessage = function(data) {
this.inactiveTimeoutStart();
this.onAgentTypingEnd(); this.onAgentTypingEnd();
this.maybeAddTimestamp(); this.maybeAddTimestamp();
return this.renderMessage({ return this.renderMessage({
@ -386,11 +418,13 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
} }
this.isOpen = true; this.isOpen = true;
if (!this.sessionId) { if (!this.sessionId) {
return this.session_init(); return this.sessionInit();
} }
}; };
ZammadChat.prototype.onOpenAnimationEnd = function() {}; ZammadChat.prototype.onOpenAnimationEnd = function() {
return this.idleTimeoutStop();
};
ZammadChat.prototype.close = function(event) { ZammadChat.prototype.close = function(event) {
if (this.state === 'off' || this.state === 'unsupported') { if (this.state === 'off' || this.state === 'unsupported') {
@ -402,10 +436,18 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
if (!this.sessionId) { if (!this.sessionId) {
return; return;
} }
this.send('chat_session_close', {
session_id: this.sessionId
});
this.inactiveTimeoutStop();
sessionStorage.removeItem('unfinished_message');
if (this.onInitialQueueDelayId) { if (this.onInitialQueueDelayId) {
clearTimeout(this.onInitialQueueDelayId); clearTimeout(this.onInitialQueueDelayId);
} }
return this.closeWindow(); if (event) {
this.closeWindow();
}
return this.setSessionId(void 0);
}; };
ZammadChat.prototype.closeWindow = function() { ZammadChat.prototype.closeWindow = function() {
@ -421,11 +463,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
this.el.removeClass('zammad-chat-is-visible'); this.el.removeClass('zammad-chat-is-visible');
this.disconnect(); this.disconnect();
this.isOpen = false; this.isOpen = false;
this.send('chat_session_close', {
session_id: this.sessionId
});
this.setSessionId(void 0);
sessionStorage.removeItem('unfinished_message');
return this.onWebSocketOpen(); return this.onWebSocketOpen();
}; };
@ -542,7 +579,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return this.el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight')); return this.el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'));
}; };
ZammadChat.prototype.session_init = function() { ZammadChat.prototype.sessionInit = function() {
return this.send('chat_session_init'); return this.send('chat_session_init');
}; };
@ -591,6 +628,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.onWebSocketOpen = function() { ZammadChat.prototype.onWebSocketOpen = function() {
this.idleTimeoutStart();
this.sessionId = sessionStorage.getItem('sessionId'); this.sessionId = sessionStorage.getItem('sessionId');
this.log('debug', 'ws connected'); this.log('debug', 'ws connected');
this.send('chat_status_customer', { this.send('chat_status_customer', {
@ -617,7 +655,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
ZammadChat.prototype.onSessionClosed = function(data) { ZammadChat.prototype.onSessionClosed = function(data) {
this.addStatus(this.T('Chat closed by %s', data.realname)); this.addStatus(this.T('Chat closed by %s', data.realname));
this.disableInput(); this.disableInput();
return this.setAgentOnlineState('offline'); this.setAgentOnlineState('offline');
return this.inactiveTimeoutStop();
}; };
ZammadChat.prototype.disconnect = function() { ZammadChat.prototype.disconnect = function() {
@ -660,11 +699,16 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
}; };
ZammadChat.prototype.showTimeout = function() { ZammadChat.prototype.showTimeout = function() {
return this.el.find('.zammad-chat-body').html(this.view('timeout')({ var reload;
this.el.find('.zammad-chat-body').html(this.view('timeout')({
agent: this.agent.name, agent: this.agent.name,
delay: 10, delay: this.options.inactiveTimeout
unit: this.T('minutes')
})); }));
this.close();
reload = function() {
return location.reload();
};
return this.el.find('.js-restart').click(reload);
}; };
ZammadChat.prototype.showLoader = function() { ZammadChat.prototype.showLoader = function() {
@ -678,6 +722,67 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
return this.el.find('.zammad-chat-agent-status').attr('data-status', state).text(this.T(capitalizedState)); return this.el.find('.zammad-chat-agent-status').attr('data-status', state).text(this.T(capitalizedState));
}; };
ZammadChat.prototype.loadCss = function() {
var newSS, styles, url;
if (!this.options.cssAutoload) {
return;
}
url = this.options.cssUrl;
if (!url) {
url = this.options.host.replace(/^wss/i, 'https').replace(/^ws/i, 'http').replace(/\/ws/i, '');
url += '/assets/chat/chat.css';
}
this.log('debug', "load css from '" + url + "'");
styles = "@import url('" + url + "');";
newSS = document.createElement('link');
newSS.rel = 'stylesheet';
newSS.href = 'data:text/css,' + escape(styles);
return document.getElementsByTagName('head')[0].appendChild(newSS);
};
ZammadChat.prototype.inactiveTimeoutStart = function() {
var delay;
this.inactiveTimeoutStop();
delay = (function(_this) {
return function() {
_this.log('debug', "Inactive timeout of " + _this.options.inactiveTimeout + " minutes, show timeout screen.");
_this.state = 'off';
_this.setAgentOnlineState('offline');
_this.showTimeout();
return _this.wsClose();
};
})(this);
return this.inactiveTimeoutStopDelayId = setTimeout(delay, this.options.inactiveTimeout * 1000 * 60);
};
ZammadChat.prototype.inactiveTimeoutStop = function() {
if (!this.inactiveTimeoutStopDelayId) {
return;
}
return clearTimeout(this.inactiveTimeoutStopDelayId);
};
ZammadChat.prototype.idleTimeoutStart = function() {
var delay;
this.idleTimeoutStop();
delay = (function(_this) {
return function() {
_this.log('debug', "Idle timeout of " + _this.options.idleTimeout + " minutes, hide widget");
_this.state = 'off';
_this.hide();
return _this.wsClose();
};
})(this);
return this.idleTimeoutStopDelayId = setTimeout(delay, this.options.idleTimeout * 1000 * 60);
};
ZammadChat.prototype.idleTimeoutStop = function() {
if (!this.idleTimeoutStopDelayId) {
return;
}
return clearTimeout(this.idleTimeoutStopDelayId);
};
return ZammadChat; return ZammadChat;
})(); })();
@ -884,7 +989,7 @@ window.zammadChatTemplates["chat"] = function (__obj) {
__out.push('>\n <div class="zammad-chat-header-controls">\n <span class="zammad-chat-agent-status zammad-chat-is-hidden" data-status="online"></span>\n <span class="zammad-chat-header-icon">\n <svg class="zammad-chat-header-icon-open" viewBox="0 0 13 7"><path d="M10.807 7l1.4-1.428-5-4.9L6.5-.02l-.7.7-4.9 4.9 1.414 1.413L6.5 2.886 10.807 7z" fill-rule="evenodd"/></svg>\n <svg class="zammad-chat-header-icon-close js-chat-close" viewBox="0 0 13 13"><path d="m2.241.12l-2.121 2.121 4.243 4.243-4.243 4.243 2.121 2.121 4.243-4.243 4.243 4.243 2.121-2.121-4.243-4.243 4.243-4.243-2.121-2.121-4.243 4.243-4.243-4.243" fill-rule="evenodd"/></svg>\n </span>\n </div>\n <div class="zammad-chat-agent zammad-chat-is-hidden">\n </div>\n <div class="zammad-chat-welcome">\n <svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg>\n <span class="zammad-chat-welcome-text">'); __out.push('>\n <div class="zammad-chat-header-controls">\n <span class="zammad-chat-agent-status zammad-chat-is-hidden" data-status="online"></span>\n <span class="zammad-chat-header-icon">\n <svg class="zammad-chat-header-icon-open" viewBox="0 0 13 7"><path d="M10.807 7l1.4-1.428-5-4.9L6.5-.02l-.7.7-4.9 4.9 1.414 1.413L6.5 2.886 10.807 7z" fill-rule="evenodd"/></svg>\n <svg class="zammad-chat-header-icon-close js-chat-close" viewBox="0 0 13 13"><path d="m2.241.12l-2.121 2.121 4.243 4.243-4.243 4.243 2.121 2.121 4.243-4.243 4.243 4.243 2.121-2.121-4.243-4.243 4.243-4.243-2.121-2.121-4.243 4.243-4.243-4.243" fill-rule="evenodd"/></svg>\n </span>\n </div>\n <div class="zammad-chat-agent zammad-chat-is-hidden">\n </div>\n <div class="zammad-chat-welcome">\n <svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg>\n <span class="zammad-chat-welcome-text">');
__out.push(this.title); __out.push(this.T(this.title));
__out.push('</span>\n </div>\n </div>\n <div class="zammad-chat-body"></div>\n <form class="zammad-chat-controls">\n <textarea class="zammad-chat-input" rows="1" placeholder="'); __out.push('</span>\n </div>\n </div>\n <div class="zammad-chat-body"></div>\n <form class="zammad-chat-controls">\n <textarea class="zammad-chat-input" rows="1" placeholder="');
@ -1128,9 +1233,17 @@ window.zammadChatTemplates["timeout"] = function (__obj) {
(function() { (function() {
__out.push('<div class="zammad-chat-modal">\n <div class="zammad-chat-modal-text">\n '); __out.push('<div class="zammad-chat-modal">\n <div class="zammad-chat-modal-text">\n ');
__out.push(this.T('Since you didn\'t respond in the last %s your conversation with <strong>%s</strong> got closed.', this.delay + " " + this.unit, this.agent)); if (this.agent) {
__out.push('\n ');
__out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.', this.delay, this.agent));
__out.push('\n ');
} else {
__out.push('\n ');
__out.push(this.T('Since you didn\'t respond in the last %s minutes your conversation got closed.', this.delay));
__out.push('\n ');
}
__out.push('<br>\n <div class="zammad-chat-button"'); __out.push('\n <br>\n <div class="zammad-chat-button js-restart"');
if (this.background) { if (this.background) {
__out.push(__sanitize(" style='background: " + this.background + "'")); __out.push(__sanitize(" style='background: " + this.background + "'"));

File diff suppressed because one or more lines are too long

View file

@ -294,6 +294,7 @@
align-items: flex-start; align-items: flex-start;
border-top: 1px solid hsl(0,0%,93%); border-top: 1px solid hsl(0,0%,93%);
padding: 0; padding: 0;
margin: 0;
line-height: 1.4em; line-height: 1.4em;
box-shadow: box-shadow:
0 1px rgba(0,0,0,.01), 0 1px rgba(0,0,0,.01),
@ -317,6 +318,7 @@
box-shadow: none; box-shadow: none;
padding: 1em 2em; padding: 1em 2em;
outline: none; outline: none;
box-shadow: none;
resize: none; resize: none;
flex: 1; flex: 1;
max-height: 6em; max-height: 6em;
@ -360,9 +362,7 @@
} }
/* /*
# Flat Design # Flat Design
*/ */
.zammad-chat--flat .zammad-chat-header, .zammad-chat--flat .zammad-chat-header,

View file

@ -1,7 +1,6 @@
<!doctype html> <!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Zammad Chat</title> <title>Zammad Chat</title>
<link rel="stylesheet" href="chat.css">
<meta name="viewport" content="width=device-width"> <meta name="viewport" content="width=device-width">
<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35738/livereload.js?snipver=1"></' + 'script>')</script> <script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35738/livereload.js?snipver=1"></' + 'script>')</script>
<style> <style>
@ -140,6 +139,7 @@
var chat = new ZammadChat({ var chat = new ZammadChat({
chatId: 1, chatId: 1,
host: 'ws://localhost:6042', host: 'ws://localhost:6042',
cssUrl: 'http://localhost:3000/assets/chat/chat.css',
debug: true debug: true
}); });

View file

@ -11,7 +11,7 @@
</div> </div>
<div class="zammad-chat-welcome"> <div class="zammad-chat-welcome">
<svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg> <svg class="zammad-chat-icon" viewBox="0 0 24 24"><path d="M2 5C2 4 3 3 4 3h16c1 0 2 1 2 2v10C22 16 21 17 20 17H4C3 17 2 16 2 15V5zM12 17l6 4v-4h-6z" fill-rule="evenodd"/></svg>
<span class="zammad-chat-welcome-text"><%- @title %></span> <span class="zammad-chat-welcome-text"><%- @T(@title) %></span>
</div> </div>
</div> </div>
<div class="zammad-chat-body"></div> <div class="zammad-chat-body"></div>

View file

@ -1,6 +1,11 @@
<div class="zammad-chat-modal"> <div class="zammad-chat-modal">
<div class="zammad-chat-modal-text"> <div class="zammad-chat-modal-text">
<%- @T('Since you didn\'t respond in the last %s your conversation with <strong>%s</strong> got closed.', "#{ @delay } #{ @unit }", @agent) %><br> <% if @agent: %>
<div class="zammad-chat-button"<%= " style='background: #{ @background }'" if @background %>><%- @T('Start new conversation') %></div> <%- @T('Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.', @delay, @agent) %>
<% else: %>
<%- @T('Since you didn\'t respond in the last %s minutes your conversation got closed.', @delay) %>
<% end %>
<br>
<div class="zammad-chat-button js-restart"<%= " style='background: #{ @background }'" if @background %>><%- @T('Start new conversation') %></div>
</div> </div>
</div> </div>

View file

@ -1,11 +1,12 @@
<!doctype html> <!doctype html>
<meta charset="utf-8"> <html lang="de-de">
<title>Zammad Chat</title> <head>
<link rel="stylesheet" href="chat.css"> <meta charset="utf-8">
<link rel="stylesheet" href="znuny.css"> <title>Zammad Chat</title>
<meta name="viewport" content="width=device-width"> <link rel="stylesheet" href="znuny.css">
<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35738/livereload.js?snipver=1"></' + 'script>')</script> <meta name="viewport" content="width=device-width">
<style> <script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':35738/livereload.js?snipver=1"></' + 'script>')</script>
<style>
body { body {
margin: 0; margin: 0;
font-family: sans-serif; font-family: sans-serif;
@ -64,8 +65,9 @@
background: hsl(0,0%,36%); background: hsl(0,0%,36%);
color: white; color: white;
} }
</style> </style>
</head>
<body>
<img class="mockup" width="100%" src="znuny.png"> <img class="mockup" width="100%" src="znuny.png">
<div class="settings"> <div class="settings">
@ -107,6 +109,7 @@
var chat = new ZammadChat({ var chat = new ZammadChat({
chatId: 1, chatId: 1,
host: 'ws://localhost:6042', host: 'ws://localhost:6042',
cssUrl: 'http://localhost:3000/assets/chat/chat.css',
debug: true, debug: true,
background: '#494d52', background: '#494d52',
flat: true, flat: true,
@ -145,3 +148,5 @@
} }
}); });
</script> </script>
</body>
</html>

29
test/fixtures/mail34.box vendored Normal file
View file

@ -0,0 +1,29 @@
From: "Bay" <memberbay+12345@members.somewhat>
Subject: strange email with empty text/plain
To: bay@example.com
Date: 11 Nov 2015 12:07:51 +0000
Priority: normal
X-Priority: 3 (Normal)
Importance: normal
X-David-SYM: 0
X-David-Flags: 0
Message-ID: <7ca10659-214c-4bd0-8438-b935a01c7601@stange>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="----_=_NextPart_000_06409CFC.56433DA7"
This message is in MIME format. Since your mail reader does not understand
this format, some or all of this message may not be legible.
------_=_NextPart_000_06409CFC.56433DA7
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable
------_=_NextPart_000_06409CFC.56433DA7
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>some html text</b>
------_=_NextPart_000_06409CFC.56433DA7

View file

@ -308,7 +308,7 @@ Hof
# spam email # spam email
{ {
data: IO.read('test/fixtures/mail15.box'), data: IO.read('test/fixtures/mail15.box'),
body_md5: 'd41d8cd98f00b204e9800998ecf8427e', body_md5: '5872ddcdfdf6bfe40f36cd0408fca667',
attachments: [ attachments: [
# :preferences=>{"Message-ID"=>"<temp@test>", "Content-Type"=>"application/octet-stream; name=\"\xBC\xA8\xD0\xA7\xB9\xDC\xC0\xED,\xBE\xBF\xBE\xB9\xCB\xAD\xB4\xED\xC1\xCB.xls\"", "Mime-Type"=>"application/octet-stream", "Charset"=>"UTF-8"}} # :preferences=>{"Message-ID"=>"<temp@test>", "Content-Type"=>"application/octet-stream; name=\"\xBC\xA8\xD0\xA7\xB9\xDC\xC0\xED,\xBE\xBF\xBE\xB9\xCB\xAD\xB4\xED\xC1\xCB.xls\"", "Mime-Type"=>"application/octet-stream", "Charset"=>"UTF-8"}}
# mutt c1abb5fb77a9d2ab2017749a7987c074 # mutt c1abb5fb77a9d2ab2017749a7987c074
@ -466,14 +466,14 @@ Freemont and pulling out several minutes.
}, },
{ {
data: IO.read('test/fixtures/mail24.box'), data: IO.read('test/fixtures/mail24.box'),
body_md5: 'd41d8cd98f00b204e9800998ecf8427e', body_md5: '5872ddcdfdf6bfe40f36cd0408fca667',
params: { params: {
from: 'oracle@IG0-1-DB01.example.com', from: 'oracle@IG0-1-DB01.example.com',
from_email: 'oracle@IG0-1-DB01.example.com', from_email: 'oracle@IG0-1-DB01.example.com',
from_display_name: '', from_display_name: '',
subject: 'Regelsets im Test-Status gefunden: 1', subject: 'Regelsets im Test-Status gefunden: 1',
to: 'support@example.com', to: 'support@example.com',
body: '', body: 'no visible content',
}, },
attachments: [ attachments: [
{ {
@ -672,9 +672,24 @@ Weil wir die Echtheit oder Vollständigkeit der in dieserNachricht enthaltenen I
to: 'info@znuny.inc', to: 'info@znuny.inc',
}, },
}, },
{
data: IO.read('test/fixtures/mail34.box'),
body_md5: 'b6e46176404ec81b3ab412fe71dff0f0',
params: {
from: 'Bay <memberbay+12345@members.somewhat>',
from_email: 'memberbay+12345@members.somewhat',
from_display_name: 'Bay',
subject: 'strange email with empty text/plain',
to: 'bay@example.com',
body: 'some html text',
},
},
] ]
count = 0
files.each { |file| files.each { |file|
count += 1
#p "Count: #{count}"
parser = Channel::EmailParser.new parser = Channel::EmailParser.new
data = parser.parse( file[:data] ) data = parser.parse( file[:data] )