feat: Display sent status of emails in email channel (#3125)

This commit is contained in:
Pranav Raj S
2021-10-14 12:55:46 +05:30
committed by GitHub
parent 5c30bc3e2b
commit 99abbb8158
18 changed files with 187 additions and 55 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",

View File

@@ -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() {

View File

@@ -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 (

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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 %>

View File

@@ -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

View 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