feat: Display sent status of emails in email channel (#3125)
This commit is contained in:
@@ -22,8 +22,8 @@
|
||||
/>
|
||||
<reply-email-head
|
||||
v-if="showReplyHead"
|
||||
@set-emails="setCcEmails"
|
||||
:clear-mails="clearMails"
|
||||
@set-emails="setCcEmails"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!showRichContentEditor"
|
||||
@@ -278,8 +278,8 @@ export default {
|
||||
}
|
||||
return !this.message.trim().replace(/\n/g, '').length;
|
||||
},
|
||||
showReplyHead(){
|
||||
return this.isAnEmailChannel;
|
||||
showReplyHead() {
|
||||
return !this.isOnPrivateNote && this.isAnEmailChannel;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -465,11 +465,11 @@ export default {
|
||||
messagePayload.file = attachment.resource.file;
|
||||
}
|
||||
|
||||
if(this.ccEmails) {
|
||||
if (this.ccEmails) {
|
||||
messagePayload.ccEmails = this.ccEmails;
|
||||
}
|
||||
|
||||
if(this.bccEmails) {
|
||||
if (this.bccEmails) {
|
||||
messagePayload.bccEmails = this.bccEmails;
|
||||
}
|
||||
|
||||
@@ -480,8 +480,8 @@ export default {
|
||||
},
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails
|
||||
}
|
||||
this.ccEmails = value.ccEmails;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<div class="message-text--metadata">
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
<span v-if="showSentIndicator" class="time">
|
||||
<i
|
||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||
class="icon ion-checkmark"
|
||||
/>
|
||||
</span>
|
||||
<i
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
@@ -36,8 +42,10 @@
|
||||
<script>
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
@@ -99,6 +107,9 @@ export default {
|
||||
return `https://twitter.com/${screenName ||
|
||||
this.inbox.name}/status/${sourceId}`;
|
||||
},
|
||||
showSentIndicator() {
|
||||
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTweetReply() {
|
||||
@@ -128,8 +139,7 @@ export default {
|
||||
}
|
||||
|
||||
.right {
|
||||
.ion-reply,
|
||||
.ion-android-open {
|
||||
.icon {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
@@ -201,4 +211,8 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delivered-icon {
|
||||
margin-left: -var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"RECEIVED_VIA_EMAIL": "Received via email",
|
||||
"VIEW_TWEET_IN_TWITTER": "View tweet in Twitter",
|
||||
"REPLY_TO_TWEET": "Reply to this tweet",
|
||||
"SENT": "Sent successfully",
|
||||
"NO_MESSAGES": "No Messages",
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
|
||||
@@ -486,6 +486,9 @@ export default {
|
||||
if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) {
|
||||
return `${this.inbox.name} (${this.inbox.phone_number})`;
|
||||
}
|
||||
if (this.isAnEmailChannel) {
|
||||
return `${this.inbox.name} (${this.inbox.email})`;
|
||||
}
|
||||
return this.inbox.name;
|
||||
},
|
||||
messengerScript() {
|
||||
|
||||
@@ -64,16 +64,17 @@ export default {
|
||||
return this.chatAdditionalAttributes.type || 'facebook';
|
||||
},
|
||||
inboxBadge() {
|
||||
const badgeKey = '';
|
||||
if (this.isATwitterInbox) {
|
||||
return this.twitterBadge;
|
||||
badgeKey = this.twitterBadge;
|
||||
} else if (this.isAFacebookInbox) {
|
||||
badgeKey = this.facebookBadge;
|
||||
} else if (this.isATwilioChannel) {
|
||||
badgeKey = this.twilioBadge;
|
||||
} else if (this.isAWhatsappChannel) {
|
||||
badgeKey = 'whatsapp';
|
||||
}
|
||||
if (this.isAFacebookInbox) {
|
||||
return this.facebookBadge;
|
||||
}
|
||||
if (this.isATwilioChannel) {
|
||||
return this.twilioBadge;
|
||||
}
|
||||
return this.channelType;
|
||||
return badgeKey || this.channelType;
|
||||
},
|
||||
isAWhatsappChannel() {
|
||||
return (
|
||||
|
||||
@@ -40,7 +40,7 @@ class ApplicationMailbox < ActionMailbox::Base
|
||||
def self.in_reply_to_mail?(inbound_mail_obj, is_a_reply_email)
|
||||
return if is_a_reply_email
|
||||
|
||||
in_reply_to = inbound_mail_obj.mail['In-Reply-To'].value
|
||||
in_reply_to = inbound_mail_obj.mail.in_reply_to
|
||||
|
||||
return false if in_reply_to.blank?
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class ReplyMailbox < ApplicationMailbox
|
||||
def find_relative_conversation
|
||||
if @conversation_uuid
|
||||
find_conversation_with_uuid
|
||||
elsif mail['In-Reply-To'].try(:value).present?
|
||||
elsif mail.in_reply_to.present?
|
||||
find_conversation_with_in_reply_to
|
||||
end
|
||||
end
|
||||
@@ -63,7 +63,7 @@ class ReplyMailbox < ApplicationMailbox
|
||||
# find conversation uuid from below pattern
|
||||
# <conversation/#{@conversation.uuid}/messages/#{@messages&.last&.id}@#{@account.inbound_email_domain}>
|
||||
def find_conversation_with_in_reply_to
|
||||
in_reply_to = mail['In-Reply-To'].try(:value)
|
||||
in_reply_to = mail.in_reply_to
|
||||
match_result = in_reply_to.match(ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN) if in_reply_to.present?
|
||||
|
||||
if match_result
|
||||
|
||||
@@ -2,14 +2,14 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
default from: ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')
|
||||
layout :choose_layout
|
||||
|
||||
def reply_with_summary(conversation, message_queued_time)
|
||||
def reply_with_summary(conversation, last_queued_id)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
init_conversation_attributes(conversation)
|
||||
return if conversation_already_viewed?
|
||||
|
||||
recap_messages = @conversation.messages.chat.where('created_at < ?', message_queued_time).last(10)
|
||||
new_messages = @conversation.messages.chat.where('created_at >= ?', message_queued_time)
|
||||
recap_messages = @conversation.messages.chat.where('id < ?', last_queued_id).last(10)
|
||||
new_messages = @conversation.messages.chat.where('id >= ?', last_queued_id)
|
||||
@messages = recap_messages + new_messages
|
||||
@messages = @messages.select(&:email_reply_summarizable?)
|
||||
|
||||
@@ -25,13 +25,13 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
})
|
||||
end
|
||||
|
||||
def reply_without_summary(conversation, message_queued_time)
|
||||
def reply_without_summary(conversation, last_queued_id)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
init_conversation_attributes(conversation)
|
||||
return if conversation_already_viewed?
|
||||
|
||||
@messages = @conversation.messages.chat.where(message_type: [:outgoing, :template]).where('created_at >= ?', message_queued_time)
|
||||
@messages = @conversation.messages.chat.where(message_type: [:outgoing, :template]).where('id >= ?', last_queued_id)
|
||||
@messages = @messages.reject { |m| m.template? && !m.input_csat? }
|
||||
return false if @messages.count.zero?
|
||||
|
||||
@@ -41,12 +41,30 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
in_reply_to: in_reply_to_email
|
||||
})
|
||||
end
|
||||
|
||||
def email_reply(message)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
init_conversation_attributes(message.conversation)
|
||||
@message = message
|
||||
|
||||
reply_mail_object = mail({
|
||||
to: @contact&.email,
|
||||
from: from_email_with_name,
|
||||
reply_to: reply_email,
|
||||
subject: mail_subject,
|
||||
message_id: custom_message_id,
|
||||
in_reply_to: in_reply_to_email,
|
||||
cc: cc_bcc_emails[0],
|
||||
bcc: cc_bcc_emails[1]
|
||||
})
|
||||
|
||||
message.update(source_id: reply_mail_object.message_id)
|
||||
end
|
||||
|
||||
def conversation_transcript(conversation, to_email)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@@ -104,7 +122,7 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
|
||||
def reply_email
|
||||
if should_use_conversation_email_address?
|
||||
"#{assignee_name} <reply+#{@conversation.uuid}@#{@account.inbound_email_domain}>"
|
||||
"#{assignee_name} from #{@inbox.name} <reply+#{@conversation.uuid}@#{@account.inbound_email_domain}>"
|
||||
else
|
||||
@inbox.email_address || @agent&.email
|
||||
end
|
||||
@@ -129,7 +147,9 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def custom_message_id
|
||||
"<conversation/#{@conversation.uuid}/messages/#{@messages&.last&.id}@#{@account.inbound_email_domain}>"
|
||||
last_message = @message || @messages&.last
|
||||
|
||||
"<conversation/#{@conversation.uuid}/messages/#{last_message&.id}@#{@account.inbound_email_domain}>"
|
||||
end
|
||||
|
||||
def in_reply_to_email
|
||||
@@ -161,7 +181,7 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def choose_layout
|
||||
return false if action_name == 'reply_without_summary'
|
||||
return false if action_name == 'reply_without_summary' || action_name == 'email_reply'
|
||||
|
||||
'mailer/base'
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ class ContactInbox < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_email_source_id
|
||||
errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Device.email_regexp}") unless Devise.email_regexp.match?(source_id)
|
||||
errors.add(:source_id, "invalid source id for Email inbox. valid Regex #{Devise.email_regexp}") unless Devise.email_regexp.match?(source_id)
|
||||
end
|
||||
|
||||
def valid_source_id_format?
|
||||
|
||||
@@ -205,17 +205,15 @@ class Message < ApplicationRecord
|
||||
end
|
||||
|
||||
def trigger_notify_via_mail
|
||||
return EmailReplyWorker.perform_in(1.second, id) if inbox.inbox_type == 'Email'
|
||||
|
||||
# will set a redis key for the conversation so that we don't need to send email for every new message
|
||||
# last few messages coupled together is sent every 2 minutes rather than one email for each message
|
||||
# if redis key exists there is an unprocessed job that will take care of delivering the email
|
||||
return if Redis::Alfred.get(conversation_mail_key).present?
|
||||
|
||||
Redis::Alfred.setex(conversation_mail_key, Time.zone.now)
|
||||
if inbox.inbox_type == 'Email'
|
||||
ConversationReplyEmailWorker.perform_in(2.seconds, conversation.id, Time.zone.now)
|
||||
else
|
||||
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now)
|
||||
end
|
||||
Redis::Alfred.setex(conversation_mail_key, id)
|
||||
ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, id)
|
||||
end
|
||||
|
||||
def conversation_mail_key
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<% if @message.content %>
|
||||
<%= CommonMarker.render_html(@message.content).html_safe %>
|
||||
<% end %>
|
||||
<% if @message.attachments %>
|
||||
<% @message.attachments.each do |attachment| %>
|
||||
attachment [<a href="<%= attachment.file_url %>" _target="blank">click here to view</a>]
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -3,14 +3,14 @@ class ConversationReplyEmailWorker
|
||||
include Sidekiq::Worker
|
||||
sidekiq_options queue: :mailers
|
||||
|
||||
def perform(conversation_id, queued_time)
|
||||
def perform(conversation_id, last_queued_id)
|
||||
@conversation = Conversation.find(conversation_id)
|
||||
|
||||
# send the email
|
||||
if @conversation.messages.incoming&.last&.content_type == 'incoming_email' || email_inbox?
|
||||
ConversationReplyMailer.with(account: @conversation.account).reply_without_summary(@conversation, queued_time).deliver_later
|
||||
if @conversation.messages.incoming&.last&.content_type == 'incoming_email'
|
||||
ConversationReplyMailer.with(account: @conversation.account).reply_without_summary(@conversation, last_queued_id).deliver_later
|
||||
else
|
||||
ConversationReplyMailer.with(account: @conversation.account).reply_with_summary(@conversation, queued_time).deliver_later
|
||||
ConversationReplyMailer.with(account: @conversation.account).reply_with_summary(@conversation, last_queued_id).deliver_later
|
||||
end
|
||||
|
||||
# delete the redis set from the first new message on the conversation
|
||||
|
||||
14
app/workers/email_reply_worker.rb
Normal file
14
app/workers/email_reply_worker.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class EmailReplyWorker
|
||||
include Sidekiq::Worker
|
||||
sidekiq_options queue: :mailers
|
||||
|
||||
def perform(message_id)
|
||||
message = Message.find(message_id)
|
||||
|
||||
return unless message.outgoing? || message.input_csat?
|
||||
return if message.private?
|
||||
|
||||
# send the email
|
||||
ConversationReplyMailer.with(account: message.account).email_reply(message).deliver_later
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user