Added basic tweet reply function.

This commit is contained in:
Martin Edenhofer 2015-12-14 17:09:09 +01:00
parent 79a32230fd
commit 0a20a1f906
7 changed files with 177 additions and 77 deletions

View file

@ -2,8 +2,10 @@ class App.TicketZoomArticleActions extends App.Controller
events: events:
'click [data-type=public]': 'publicInternal' 'click [data-type=public]': 'publicInternal'
'click [data-type=internal]': 'publicInternal' 'click [data-type=internal]': 'publicInternal'
'click [data-type=reply]': 'reply' 'click [data-type=emailReply]': 'emailReply'
'click [data-type=replyAll]': 'replyAll' 'click [data-type=emailReplyAll]': 'emailReplyAll'
'click [data-type=twitterStatusReply]': 'twitterStatusReply'
'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply'
constructor: -> constructor: ->
super super
@ -68,7 +70,7 @@ class App.TicketZoomArticleActions extends App.Controller
if article.type.name is 'email' || article.type.name is 'phone' || article.type.name is 'web' if article.type.name is 'email' || article.type.name is 'phone' || article.type.name is 'web'
actions.push { actions.push {
name: 'reply' name: 'reply'
type: 'reply' type: 'emailReply'
icon: 'reply' icon: 'reply'
href: '#' href: '#'
} }
@ -90,10 +92,18 @@ class App.TicketZoomArticleActions extends App.Controller
if recipients.length > 1 if recipients.length > 1
actions.push { actions.push {
name: 'reply all' name: 'reply all'
type: 'replyAll' type: 'emailReplyAll'
icon: 'reply-all' icon: 'reply-all'
href: '#' href: '#'
} }
if article.type.name is 'twitter status'
actions.push {
name: 'reply'
type: 'twitterStatusReply'
icon: 'reply'
href: '#'
}
actions.push { actions.push {
name: 'split' name: 'split'
type: 'split' type: 'split'
@ -102,19 +112,83 @@ class App.TicketZoomArticleActions extends App.Controller
} }
actions actions
replyAll: (e) => twitterStatusReply: (e) =>
@reply(e, true)
reply: (e, all = false) =>
e.preventDefault() e.preventDefault()
# get reference article # get reference article
article_id = $(e.target).parents('[data-id]').data('id') article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal( article_id ) article = App.TicketArticle.fullLocal(article_id)
type = App.TicketArticleType.find( article.type_id ) type = App.TicketArticleType.find(article.type_id)
customer = App.User.find( article.created_by_id ) customer = App.User.find(article.created_by_id)
@el.closest('.article-add').ScrollTo() @scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
articleNew.body = body
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
recipient = "@#{to} "
if !body
articleNew.body = recipient
if body && !body.match("@#{to}")
articleNew.body = "#{recipient}#{articleNew.body}"
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
twitterDirectMessageReply: (e) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
type = App.TicketArticleType.find(article.type_id)
customer = App.User.find(article.created_by_id)
@scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
emailReplyAll: (e) =>
@emailReply(e, true)
emailReply: (e, all = false) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
type = App.TicketArticleType.find(article.type_id)
customer = App.User.find(article.created_by_id)
@scrollToCompose()
# empty form # empty form
articleNew = { articleNew = {
@ -129,19 +203,7 @@ class App.TicketZoomArticleActions extends App.Controller
if article.message_id if article.message_id
articleNew.in_reply_to = article.message_id articleNew.in_reply_to = article.message_id
if type.name is 'twitter status' if type.name is 'email' || type.name is 'phone' || type.name is 'web'
# set to in body
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.body = '@' + to
else if type.name is 'twitter direct-message'
# show to
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.to = to
else if type.name is 'email' || type.name is 'phone' || type.name is 'web'
if article.sender.name is 'Agent' if article.sender.name is 'Agent'
articleNew.to = article.to articleNew.to = article.to
@ -198,10 +260,10 @@ class App.TicketZoomArticleActions extends App.Controller
if selectedText if selectedText
# clean selection # clean selection
selectedText = App.Utils.textCleanup( selectedText ) selectedText = App.Utils.textCleanup(selectedText)
# convert to html # convert to html
selectedText = App.Utils.text2html( selectedText ) selectedText = App.Utils.text2html(selectedText)
if selectedText if selectedText
selectedText = "<div><br><br/></div><div><blockquote type=\"cite\">#{selectedText}</blockquote></div><div><br></div>" selectedText = "<div><br><br/></div><div><blockquote type=\"cite\">#{selectedText}</blockquote></div><div><br></div>"
@ -210,4 +272,10 @@ class App.TicketZoomArticleActions extends App.Controller
articleNew.body = body articleNew.body = body
type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
scrollToCompose: =>
@el.closest('.content').find('.article-add').ScrollTo()

View file

@ -84,7 +84,6 @@ class App.TicketZoomArticleNew extends App.Controller
# set article type and expand text area # set article type and expand text area
@bind('ui::ticket::setArticleType', (data) => @bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id isnt @ticket_id return if data.ticket.id isnt @ticket_id
#@setArticleType(data.type.name)
@openTextarea(null, true) @openTextarea(null, true)
for key, value of data.article for key, value of data.article
@ -94,7 +93,7 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[name="' + key + '"]').val(value) @$('[name="' + key + '"]').val(value)
# preselect article type # preselect article type
@setArticleType('email') @setArticleType(data.type.name)
) )
# reset new article screen # reset new article screen

View file

@ -8,12 +8,11 @@ class Avatar < ApplicationModel
add an avatar based on auto detection (email address) add an avatar based on auto detection (email address)
Avatar.auto_detection( Avatar.auto_detection(
:object => 'User', object: 'User',
:o_id => user.id, o_id: user.id,
:url => 'somebody@example.com', url: 'somebody@example.com',
:source => 'web', updated_by_id: 1,
:updated_by_id => 1, created_by_id: 1,
:created_by_id => 1,
) )
=end =end
@ -41,20 +40,20 @@ add an avatar based on auto detection (email address)
add a avatar add a avatar
Avatar.add( Avatar.add(
:object => 'User', object: 'User',
:o_id => user.id, o_id: user.id,
:default => true, default: true,
:full => { full: {
:content => '...', content: '...',
:mime_type => 'image/png', mime_type: 'image/png',
}, },
:resize => { resize: {
:content => '...', content: '...',
:mime_type => 'image/png', mime_type: 'image/png',
}, },
:source => 'web', source: 'web',
:updated_by_id => 1, updated_by_id: 1,
:created_by_id => 1, created_by_id: 1,
) )
=end =end
@ -214,7 +213,7 @@ add a avatar
set avatars as default set avatars as default
Avatar.set_default( 'User', 123, avatar_id ) Avatar.set_default('User', 123, avatar_id)
=end =end
@ -238,12 +237,12 @@ set avatars as default
remove all avatars of an object remove all avatars of an object
Avatar.remove( 'User', 123 ) Avatar.remove('User', 123)
=end =end
def self.remove( object_name, o_id ) def self.remove(object_name, o_id)
object_id = ObjectLookup.by_name( object_name ) object_id = ObjectLookup.by_name(object_name)
Avatar.where( Avatar.where(
object_lookup_id: object_id, object_lookup_id: object_id,
o_id: o_id, o_id: o_id,
@ -264,12 +263,12 @@ remove all avatars of an object
remove one avatars of an object remove one avatars of an object
Avatar.remove_one( 'User', 123, avatar_id ) Avatar.remove_one('User', 123, avatar_id)
=end =end
def self.remove_one( object_name, o_id, avatar_id ) def self.remove_one(object_name, o_id, avatar_id)
object_id = ObjectLookup.by_name( object_name ) object_id = ObjectLookup.by_name(object_name)
Avatar.where( Avatar.where(
object_lookup_id: object_id, object_lookup_id: object_id,
o_id: o_id, o_id: o_id,
@ -281,16 +280,16 @@ remove one avatars of an object
return all avatars of an user return all avatars of an user
avatars = Avatar.list( 'User', 123 ) avatars = Avatar.list('User', 123)
=end =end
def self.list(object_name, o_id) def self.list(object_name, o_id)
object_id = ObjectLookup.by_name( object_name ) object_id = ObjectLookup.by_name(object_name)
avatars = Avatar.where( avatars = Avatar.where(
object_lookup_id: object_id, object_lookup_id: object_id,
o_id: o_id, o_id: o_id,
).order( 'initial DESC, deletable ASC, created_at ASC, id DESC' ) ).order('initial DESC, deletable ASC, created_at ASC, id DESC')
# add initial avatar # add initial avatar
add_init_avatar(object_id, o_id) add_init_avatar(object_id, o_id)
@ -300,7 +299,7 @@ return all avatars of an user
data = avatar.attributes data = avatar.attributes
if avatar.store_resize_id if avatar.store_resize_id
file = Store.find(avatar.store_resize_id) file = Store.find(avatar.store_resize_id)
data['content'] = "data:#{file.preferences['Mime-Type']};base64,#{Base64.strict_encode64( file.content )}" data['content'] = "data:#{file.preferences['Mime-Type']};base64,#{Base64.strict_encode64(file.content)}"
end end
avatar_list.push data avatar_list.push data
end end
@ -311,7 +310,7 @@ return all avatars of an user
get default avatar image of user by hash get default avatar image of user by hash
store = Avatar.get_by_hash( hash ) store = Avatar.get_by_hash(hash)
returns: returns:
@ -331,7 +330,7 @@ returns:
get default avatar of user by user id get default avatar of user by user id
avatar = Avatar.get_default( 'User', user_id ) avatar = Avatar.get_default('User', user_id)
returns: returns:
@ -340,7 +339,7 @@ returns:
=end =end
def self.get_default(object_name, o_id) def self.get_default(object_name, o_id)
object_id = ObjectLookup.by_name( object_name ) object_id = ObjectLookup.by_name(object_name)
Avatar.find_by( Avatar.find_by(
object_lookup_id: object_id, object_lookup_id: object_id,
o_id: o_id, o_id: o_id,
@ -352,7 +351,7 @@ returns:
avatars = Avatar.where( avatars = Avatar.where(
object_lookup_id: object_id, object_lookup_id: object_id,
o_id: o_id, o_id: o_id,
).order( 'created_at ASC, id DESC' ) ).order('created_at ASC, id DESC')
avatars.each do |avatar| avatars.each do |avatar|
next if avatar.id == avatar_id next if avatar.id == avatar_id
avatar.default = false avatar.default = false

View file

@ -1,8 +1,5 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
require 'channel/driver/twitter'
class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
observe 'ticket::_article' observe 'ticket::_article'
@ -28,9 +25,26 @@ class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
tweet = channel.deliver( tweet = channel.deliver(
type: type['name'], type: type['name'],
to: record.to, to: record.to,
body: record.body, body: record.body.html2text,
in_reply_to: record.in_reply_to in_reply_to: record.in_reply_to
) )
# fill article with tweet info
record.from = tweet.user.screen_name
if tweet.user_mentions
to = ''
twitter_mention_ids = []
tweet.user_mentions.each {|user|
if to != ''
to += ' '
end
to += "@#{user.screen_name}"
twitter_mention_ids.push user.id
}
record.to = to
record.preferences[:twitter_mention_ids] = twitter_mention_ids
end
record.message_id = tweet.id record.message_id = tweet.id
record.save record.save
end end

View file

@ -68,6 +68,24 @@ class Tweet
user = User.create(user_data) user = User.create(user_data)
end end
if user_data[:image_source]
avatar = Avatar.add(
object: 'User',
o_id: user.id,
url: user_data[:image_source],
source: 'twitter',
deletable: true,
updated_by_id: user.id,
created_by_id: user.id,
)
# update user link
if avatar && user.image != avatar.store_hash
user.image = avatar.store_hash
user.save
end
end
# create or update authorization # create or update authorization
auth_data = { auth_data = {
uid: tweet_user.id, uid: tweet_user.id,
@ -81,8 +99,6 @@ class Tweet
Authorization.create(auth_data) Authorization.create(auth_data)
end end
UserInfo.current_user_id = user.id
user user
end end
@ -112,6 +128,8 @@ class Tweet
end end
end end
UserInfo.current_user_id = user.id
Ticket.create( Ticket.create(
customer_id: user.id, customer_id: user.id,
title: "#{tweet.text[0, 37]}...", title: "#{tweet.text[0, 37]}...",
@ -148,12 +166,14 @@ class Tweet
from = tweet.sender.screen_name from = tweet.sender.screen_name
elsif tweet.class == Twitter::Tweet elsif tweet.class == Twitter::Tweet
article_type = 'twitter status' article_type = 'twitter status'
from = tweet.in_reply_to_screen_name from = tweet.user.screen_name
in_reply_to = tweet.in_reply_to_status_id in_reply_to = tweet.in_reply_to_status_id
else else
fail "Unknown tweet type '#{tweet.class}'" fail "Unknown tweet type '#{tweet.class}'"
end end
UserInfo.current_user_id = user.id
Ticket::Article.create( Ticket::Article.create(
from: from, from: from,
to: to, to: to,

View file

@ -243,7 +243,7 @@ class AgentTicketActionLevel5Test < TestCase
# execute reply # execute reply
click( click(
css: '.active [data-type="reply"]', css: '.active [data-type="emailReply"]',
) )
# check if signature exists # check if signature exists
@ -267,7 +267,7 @@ class AgentTicketActionLevel5Test < TestCase
# execute reply # execute reply
sleep 5 # time to recognice form changes sleep 5 # time to recognice form changes
click( click(
css: '.active [data-type="reply"]', css: '.active [data-type="emailReply"]',
) )
# check if signature exists # check if signature exists

View file

@ -35,11 +35,11 @@ class AgentTicketActionLevel7Test < TestCase
# scroll to reply - needed for chrome # scroll to reply - needed for chrome
scroll_to( scroll_to(
position: 'botton', position: 'botton',
css: '.content.active [data-type="reply"]', css: '.content.active [data-type="emailReply"]',
) )
# click reply # click reply
click( css: '.content.active [data-type="reply"]' ) click( css: '.content.active [data-type="emailReply"]' )
# check body # check body
watch_for( watch_for(
@ -58,11 +58,11 @@ class AgentTicketActionLevel7Test < TestCase
# scroll to reply - needed for chrome # scroll to reply - needed for chrome
scroll_to( scroll_to(
position: 'botton', position: 'botton',
css: '.content.active [data-type="reply"]', css: '.content.active [data-type="emailReply"]',
) )
# click reply # click reply
click( css: '.content.active [data-type="reply"]' ) click( css: '.content.active [data-type="emailReply"]' )
# check body # check body
watch_for( watch_for(