mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +00:00 
			
		
		
		
	feat: Notification on new messages in conversation (#1204)
fixes: #895 fixes: #1118 fixes: #1075 Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
		| @@ -75,6 +75,7 @@ Metrics/AbcSize: | ||||
|     - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' | ||||
|     - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' | ||||
| Metrics/CyclomaticComplexity: | ||||
|   Max: 7 | ||||
|   Exclude: | ||||
|     - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' | ||||
| Rails/ReversibleMigration: | ||||
|   | ||||
							
								
								
									
										104
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -18,56 +18,56 @@ GEM | ||||
|   specs: | ||||
|     action-cable-testing (0.6.1) | ||||
|       actioncable (>= 5.0) | ||||
|     actioncable (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|     actioncable (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       nio4r (~> 2.0) | ||||
|       websocket-driver (>= 0.6.1) | ||||
|     actionmailbox (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       activejob (= 6.0.3.2) | ||||
|       activerecord (= 6.0.3.2) | ||||
|       activestorage (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     actionmailbox (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       activejob (= 6.0.3.3) | ||||
|       activerecord (= 6.0.3.3) | ||||
|       activestorage (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       mail (>= 2.7.1) | ||||
|     actionmailer (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       actionview (= 6.0.3.2) | ||||
|       activejob (= 6.0.3.2) | ||||
|     actionmailer (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       actionview (= 6.0.3.3) | ||||
|       activejob (= 6.0.3.3) | ||||
|       mail (~> 2.5, >= 2.5.4) | ||||
|       rails-dom-testing (~> 2.0) | ||||
|     actionpack (6.0.3.2) | ||||
|       actionview (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     actionpack (6.0.3.3) | ||||
|       actionview (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       rack (~> 2.0, >= 2.0.8) | ||||
|       rack-test (>= 0.6.3) | ||||
|       rails-dom-testing (~> 2.0) | ||||
|       rails-html-sanitizer (~> 1.0, >= 1.2.0) | ||||
|     actiontext (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       activerecord (= 6.0.3.2) | ||||
|       activestorage (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     actiontext (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       activerecord (= 6.0.3.3) | ||||
|       activestorage (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       nokogiri (>= 1.8.5) | ||||
|     actionview (6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     actionview (6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       builder (~> 3.1) | ||||
|       erubi (~> 1.4) | ||||
|       rails-dom-testing (~> 2.0) | ||||
|       rails-html-sanitizer (~> 1.1, >= 1.2.0) | ||||
|     activejob (6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     activejob (6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       globalid (>= 0.3.6) | ||||
|     activemodel (6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     activerecord (6.0.3.2) | ||||
|       activemodel (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     activestorage (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       activejob (= 6.0.3.2) | ||||
|       activerecord (= 6.0.3.2) | ||||
|     activemodel (6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|     activerecord (6.0.3.3) | ||||
|       activemodel (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|     activestorage (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       activejob (= 6.0.3.3) | ||||
|       activerecord (= 6.0.3.3) | ||||
|       marcel (~> 0.3.1) | ||||
|     activesupport (6.0.3.2) | ||||
|     activesupport (6.0.3.3) | ||||
|       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||
|       i18n (>= 0.7, < 2) | ||||
|       minitest (~> 5.1) | ||||
| @@ -299,7 +299,7 @@ GEM | ||||
|     mini_magick (4.10.1) | ||||
|     mini_mime (1.0.2) | ||||
|     mini_portile2 (2.4.0) | ||||
|     minitest (5.14.1) | ||||
|     minitest (5.14.2) | ||||
|     momentjs-rails (2.20.1) | ||||
|       railties (>= 3.1) | ||||
|     msgpack (1.3.3) | ||||
| @@ -307,7 +307,7 @@ GEM | ||||
|     multi_xml (0.6.0) | ||||
|     multipart-post (2.1.1) | ||||
|     netrc (0.11.0) | ||||
|     nio4r (2.5.2) | ||||
|     nio4r (2.5.3) | ||||
|     nokogiri (1.10.10) | ||||
|       mini_portile2 (~> 2.4.0) | ||||
|     oauth (0.5.4) | ||||
| @@ -336,29 +336,29 @@ GEM | ||||
|       rack | ||||
|     rack-test (1.1.0) | ||||
|       rack (>= 1.0, < 3) | ||||
|     rails (6.0.3.2) | ||||
|       actioncable (= 6.0.3.2) | ||||
|       actionmailbox (= 6.0.3.2) | ||||
|       actionmailer (= 6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       actiontext (= 6.0.3.2) | ||||
|       actionview (= 6.0.3.2) | ||||
|       activejob (= 6.0.3.2) | ||||
|       activemodel (= 6.0.3.2) | ||||
|       activerecord (= 6.0.3.2) | ||||
|       activestorage (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     rails (6.0.3.3) | ||||
|       actioncable (= 6.0.3.3) | ||||
|       actionmailbox (= 6.0.3.3) | ||||
|       actionmailer (= 6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       actiontext (= 6.0.3.3) | ||||
|       actionview (= 6.0.3.3) | ||||
|       activejob (= 6.0.3.3) | ||||
|       activemodel (= 6.0.3.3) | ||||
|       activerecord (= 6.0.3.3) | ||||
|       activestorage (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       bundler (>= 1.3.0) | ||||
|       railties (= 6.0.3.2) | ||||
|       railties (= 6.0.3.3) | ||||
|       sprockets-rails (>= 2.0.0) | ||||
|     rails-dom-testing (2.0.3) | ||||
|       activesupport (>= 4.2.0) | ||||
|       nokogiri (>= 1.6) | ||||
|     rails-html-sanitizer (1.3.0) | ||||
|       loofah (~> 2.3) | ||||
|     railties (6.0.3.2) | ||||
|       actionpack (= 6.0.3.2) | ||||
|       activesupport (= 6.0.3.2) | ||||
|     railties (6.0.3.3) | ||||
|       actionpack (= 6.0.3.3) | ||||
|       activesupport (= 6.0.3.3) | ||||
|       method_source | ||||
|       rake (>= 0.8.7) | ||||
|       thor (>= 0.20.3, < 2.0) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController | ||||
|   def update_last_seen | ||||
|     head :ok && return if conversation.nil? | ||||
|  | ||||
|     conversation.user_last_seen_at = DateTime.now.utc | ||||
|     conversation.contact_last_seen_at = DateTime.now.utc | ||||
|     conversation.save! | ||||
|     head :ok | ||||
|   end | ||||
|   | ||||
| @@ -26,7 +26,8 @@ | ||||
|         "TITLE": "Email Notifications", | ||||
|         "NOTE": "Update your email notification preferences here", | ||||
|         "CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me", | ||||
|         "CONVERSATION_CREATION": "Send email notifications when a new conversation is created" | ||||
|         "CONVERSATION_CREATION": "Send email notifications when a new conversation is created", | ||||
|         "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation" | ||||
|       }, | ||||
|       "API": { | ||||
|         "UPDATE_SUCCESS": "Your notification preferences are updated successfully", | ||||
| @@ -37,6 +38,7 @@ | ||||
|         "NOTE": "Update your push notification preferences here", | ||||
|         "CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me", | ||||
|         "CONVERSATION_CREATION": "Send push notifications when a new conversation is created", | ||||
|         "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation", | ||||
|         "HAS_ENABLED_PUSH": "You have enabled push for this browser.", | ||||
|         "REQUEST_PUSH": "Enable push notifications" | ||||
|       }, | ||||
|   | ||||
| @@ -43,6 +43,23 @@ | ||||
|             }} | ||||
|           </label> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|           <input | ||||
|             v-model="selectedEmailFlags" | ||||
|             class="notification--checkbox" | ||||
|             type="checkbox" | ||||
|             value="email_assigned_conversation_new_message" | ||||
|             @input="handleEmailInput" | ||||
|           /> | ||||
|           <label for="assigned_conversation_new_message"> | ||||
|             {{ | ||||
|               $t( | ||||
|                 'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE' | ||||
|               ) | ||||
|             }} | ||||
|           </label> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="vapidPublicKey" class="profile--settings--row row push-row"> | ||||
| @@ -105,6 +122,23 @@ | ||||
|             }} | ||||
|           </label> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|           <input | ||||
|             v-model="selectedPushFlags" | ||||
|             class="notification--checkbox" | ||||
|             type="checkbox" | ||||
|             value="push_assigned_conversation_new_message" | ||||
|             @input="handlePushInput" | ||||
|           /> | ||||
|           <label for="assigned_conversation_new_message"> | ||||
|             {{ | ||||
|               $t( | ||||
|                 'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.ASSIGNED_CONVERSATION_NEW_MESSAGE' | ||||
|               ) | ||||
|             }} | ||||
|           </label> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -35,7 +35,7 @@ export default [ | ||||
|     inbox_id: 1, | ||||
|     status: 0, | ||||
|     timestamp: 1578555084, | ||||
|     user_last_seen_at: 0, | ||||
|     contact_last_seen_at: 0, | ||||
|     agent_last_seen_at: 1578555084, | ||||
|     unread_count: 0, | ||||
|   }, | ||||
| @@ -75,7 +75,7 @@ export default [ | ||||
|     inbox_id: 2, | ||||
|     status: 0, | ||||
|     timestamp: 1578555084, | ||||
|     user_last_seen_at: 0, | ||||
|     contact_last_seen_at: 0, | ||||
|     agent_last_seen_at: 1578555084, | ||||
|     unread_count: 0, | ||||
|   }, | ||||
|   | ||||
| @@ -33,7 +33,7 @@ const toggleTyping = async ({ typingStatus }) => { | ||||
| const setUserLastSeenAt = async ({ lastSeen }) => { | ||||
|   return API.post( | ||||
|     `/api/v1/widget/conversations/update_last_seen${window.location.search}`, | ||||
|     { user_last_seen_at: lastSeen } | ||||
|     { contact_last_seen_at: lastSeen } | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ export const actions = { | ||||
|   get: async ({ commit }) => { | ||||
|     try { | ||||
|       const { data } = await getConversationAPI(); | ||||
|       const { user_last_seen_at: lastSeen } = data; | ||||
|       const { contact_last_seen_at: lastSeen } = data; | ||||
|       commit(SET_CONVERSATION_ATTRIBUTES, data); | ||||
|       commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); | ||||
|     } catch (error) { | ||||
|   | ||||
| @@ -4,7 +4,7 @@ class ContactAvatarJob < ApplicationJob | ||||
|   def perform(contact, avatar_url) | ||||
|     avatar_resource = LocalResource.new(avatar_url) | ||||
|     contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) | ||||
|   rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError => e | ||||
|     Rails.logger.info "invalid url #{file_url} : #{e.message}" | ||||
|   rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError, NoMethodError => e | ||||
|     Rails.logger.info "invalid url #{avatar_url} : #{e.message}" | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -2,6 +2,9 @@ class Notification::EmailNotificationJob < ApplicationJob | ||||
|   queue_as :default | ||||
|  | ||||
|   def perform(notification) | ||||
|     # no need to send email if notification has been read already | ||||
|     return if notification.read_at.present? | ||||
|  | ||||
|     Notification::EmailNotificationService.new(notification: notification).perform | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -26,4 +26,20 @@ class NotificationListener < BaseListener | ||||
|       primary_actor: conversation | ||||
|     ).perform | ||||
|   end | ||||
|  | ||||
|   def message_created(event) | ||||
|     message, account = extract_message_and_account(event) | ||||
|     conversation = message.conversation | ||||
|  | ||||
|     # only want to notify agents about customer messages | ||||
|     return unless message.incoming? | ||||
|     return unless conversation.assignee | ||||
|  | ||||
|     NotificationBuilder.new( | ||||
|       notification_type: 'assigned_conversation_new_message', | ||||
|       user: conversation.assignee, | ||||
|       account: account, | ||||
|       primary_actor: conversation | ||||
|     ).perform | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -19,6 +19,18 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer | ||||
|     send_mail_with_liquid(to: @agent.email, subject: subject) and return | ||||
|   end | ||||
|  | ||||
|   def assigned_conversation_new_message(conversation, agent) | ||||
|     return unless smtp_config_set_or_development? | ||||
|     # Don't spam with email notifications if agent is online | ||||
|     return if ::OnlineStatusTracker.get_presence(conversation.account.id, 'User', agent.id) | ||||
|  | ||||
|     @agent = agent | ||||
|     @conversation = conversation | ||||
|     subject = "#{@agent.available_name}, New message in your assigned conversation [ID - #{@conversation.display_id}]." | ||||
|     @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) | ||||
|     send_mail_with_liquid(to: @agent.email, subject: subject) and return | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def liquid_droppables | ||||
|   | ||||
| @@ -50,7 +50,13 @@ class ApplicationMailer < ActionMailer::Base | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def locale_from_account(account) | ||||
|     I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil | ||||
|   end | ||||
|  | ||||
|   def ensure_current_account(account) | ||||
|     Current.account = account if account.present? | ||||
|     locale ||= locale_from_account(account) if account.present? | ||||
|     I18n.locale = locale || I18n.default_locale | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -6,6 +6,7 @@ class ConversationReplyMailer < ApplicationMailer | ||||
|     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) | ||||
| @@ -26,6 +27,7 @@ class ConversationReplyMailer < ApplicationMailer | ||||
|     return unless smtp_config_set_or_development? | ||||
|  | ||||
|     init_conversation_attributes(conversation) | ||||
|     return if conversation_already_viewed? | ||||
|  | ||||
|     @messages = @conversation.messages.chat.outgoing.where('created_at >= ?', message_queued_time) | ||||
|     return false if @messages.count.zero? | ||||
| @@ -63,6 +65,18 @@ class ConversationReplyMailer < ApplicationMailer | ||||
|     @agent = @conversation.assignee | ||||
|   end | ||||
|  | ||||
|   def conversation_already_viewed? | ||||
|     # whether contact already saw the message on widget | ||||
|     return unless @conversation.contact_last_seen_at | ||||
|     return unless last_outgoing_message&.created_at | ||||
|  | ||||
|     @conversation.contact_last_seen_at > last_outgoing_message&.created_at | ||||
|   end | ||||
|  | ||||
|   def last_outgoing_message | ||||
|     @conversation.messages.chat.where.not(message_type: :incoming)&.last | ||||
|   end | ||||
|  | ||||
|   def assignee_name | ||||
|     @assignee_name ||= @agent&.available_name || 'Notifications' | ||||
|   end | ||||
|   | ||||
| @@ -5,10 +5,10 @@ | ||||
| #  id                    :integer          not null, primary key | ||||
| #  additional_attributes :jsonb | ||||
| #  agent_last_seen_at    :datetime | ||||
| #  contact_last_seen_at  :datetime | ||||
| #  identifier            :string | ||||
| #  locked                :boolean          default(FALSE) | ||||
| #  status                :integer          default("open"), not null | ||||
| #  user_last_seen_at     :datetime | ||||
| #  uuid                  :uuid             not null | ||||
| #  created_at            :datetime         not null | ||||
| #  updated_at            :datetime         not null | ||||
| @@ -166,7 +166,7 @@ class Conversation < ApplicationRecord | ||||
|     { | ||||
|       CONVERSATION_OPENED => -> { saved_change_to_status? && open? }, | ||||
|       CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? }, | ||||
|       CONVERSATION_READ => -> { saved_change_to_user_last_seen_at? }, | ||||
|       CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? }, | ||||
|       CONVERSATION_LOCK_TOGGLE => -> { saved_change_to_locked? }, | ||||
|       ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }, | ||||
|       CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? } | ||||
|   | ||||
| @@ -150,14 +150,28 @@ class Message < ApplicationRecord | ||||
|     ::MessageTemplates::HookExecutionService.new(message: self).perform | ||||
|   end | ||||
|  | ||||
|   def notify_via_mail | ||||
|     if Redis::Alfred.get(conversation_mail_key).nil? && conversation.contact.email? && outgoing? && !private | ||||
|       # set a redis key for the conversation so that we don't need to send email for every | ||||
|       # new message that comes in and we dont enque the delayed sidekiq job for every message | ||||
|       Redis::Alfred.setex(conversation_mail_key, Time.zone.now) | ||||
|   def email_notifiable_message? | ||||
|     return false unless outgoing? | ||||
|     return false if private? | ||||
|  | ||||
|       # Since this is live chat, send the email after few minutes so the only one email with | ||||
|       # last few messages coupled together is sent rather than email for each message | ||||
|     true | ||||
|   end | ||||
|  | ||||
|   def can_notify_via_mail? | ||||
|     return unless email_notifiable_message? | ||||
|     return false if conversation.contact.email.blank? | ||||
|     return false unless %w[Website Email].include? inbox.inbox_type | ||||
|  | ||||
|     true | ||||
|   end | ||||
|  | ||||
|   def notify_via_mail | ||||
|     return unless can_notify_via_mail? | ||||
|  | ||||
|     # 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::Alfred.get(conversation_mail_key).nil? | ||||
|       Redis::Alfred.setex(conversation_mail_key, Time.zone.now) | ||||
|       ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now) | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -31,7 +31,8 @@ class Notification < ApplicationRecord | ||||
|  | ||||
|   NOTIFICATION_TYPES = { | ||||
|     conversation_creation: 1, | ||||
|     conversation_assignment: 2 | ||||
|     conversation_assignment: 2, | ||||
|     assigned_conversation_new_message: 3 | ||||
|   }.freeze | ||||
|  | ||||
|   enum notification_type: NOTIFICATION_TYPES | ||||
| @@ -64,6 +65,8 @@ class Notification < ApplicationRecord | ||||
|  | ||||
|     return "A new conversation [ID -#{primary_actor.display_id}] has been assigned to you." if notification_type == 'conversation_assignment' | ||||
|  | ||||
|     return "New message in your assigned conversation [ID -#{primary_actor.display_id}]." if notification_type == 'assigned_conversation_new_message' | ||||
|  | ||||
|     '' | ||||
|   end | ||||
|  | ||||
| @@ -71,6 +74,7 @@ class Notification < ApplicationRecord | ||||
|  | ||||
|   def process_notification_delivery | ||||
|     Notification::PushNotificationJob.perform_later(self) | ||||
|  | ||||
|     # Should we do something about the case where user subscribed to both push and email ? | ||||
|     # In future, we could probably add condition here to enqueue the job for 30 seconds later | ||||
|     # when push enabled and then check in email job whether notification has been read already. | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class Conversations::EventDataPresenter < SimpleDelegator | ||||
|   def push_timestamps | ||||
|     { | ||||
|       agent_last_seen_at: agent_last_seen_at.to_i, | ||||
|       user_last_seen_at: user_last_seen_at.to_i, | ||||
|       contact_last_seen_at: contact_last_seen_at.to_i, | ||||
|       timestamp: created_at.to_i | ||||
|     } | ||||
|   end | ||||
|   | ||||
| @@ -64,6 +64,8 @@ class Notification::PushNotificationService | ||||
|     ) | ||||
|   rescue Webpush::ExpiredSubscription | ||||
|     subscription.destroy! | ||||
|   rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e | ||||
|     Rails.logger.info "Webpush operation error: #{e.message}" | ||||
|   end | ||||
|  | ||||
|   def send_fcm_push(subscription) | ||||
|   | ||||
| @@ -22,7 +22,7 @@ json.status conversation.status | ||||
| json.muted conversation.muted? | ||||
| json.can_reply conversation.can_reply? | ||||
| json.timestamp conversation.messages.last.try(:created_at).try(:to_i) | ||||
| json.user_last_seen_at conversation.user_last_seen_at.to_i | ||||
| json.contact_last_seen_at conversation.contact_last_seen_at.to_i | ||||
| json.agent_last_seen_at conversation.agent_last_seen_at.to_i | ||||
| json.unread_count conversation.unread_incoming_messages.count | ||||
| json.additional_attributes conversation.additional_attributes | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| if @conversation | ||||
|   json.id @conversation.display_id | ||||
|   json.inbox_id @conversation.inbox_id | ||||
|   json.user_last_seen_at @conversation.user_last_seen_at.to_i | ||||
|   json.contact_last_seen_at @conversation.contact_last_seen_at.to_i | ||||
|   json.status @conversation.status | ||||
| end | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| <p>Hi {{user.available_name}},</p> | ||||
|  | ||||
| <p>You have received a new message in your assigned conversation.</p> | ||||
|  | ||||
| <p> | ||||
| Click <a href="{{action_url}}">here</a> to get cracking. | ||||
| </p> | ||||
							
								
								
									
										5
									
								
								db/migrate/20200907094912_rename_user_last_seen.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20200907094912_rename_user_last_seen.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| class RenameUserLastSeen < ActiveRecord::Migration[6.0] | ||||
|   def change | ||||
|     rename_column :conversations, :user_last_seen_at, :contact_last_seen_at | ||||
|   end | ||||
| end | ||||
| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 2020_08_28_175931) do | ||||
| ActiveRecord::Schema.define(version: 2020_09_07_094912) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "pgcrypto" | ||||
| @@ -219,7 +219,7 @@ ActiveRecord::Schema.define(version: 2020_08_28_175931) do | ||||
|     t.datetime "updated_at", null: false | ||||
|     t.bigint "contact_id" | ||||
|     t.integer "display_id", null: false | ||||
|     t.datetime "user_last_seen_at" | ||||
|     t.datetime "contact_last_seen_at" | ||||
|     t.datetime "agent_last_seen_at" | ||||
|     t.boolean "locked", default: false | ||||
|     t.jsonb "additional_attributes" | ||||
|   | ||||
| @@ -33,7 +33,7 @@ When a new message is created in the API channel, you will get a POST request to | ||||
|     "inbox_id": 0, | ||||
|     "status": "open", | ||||
|     "agent_last_seen_at": 0, | ||||
|     "user_last_seen_at": 0, | ||||
|     "contact_last_seen_at": 0, | ||||
|     "timestamp": 0 | ||||
|   }, | ||||
|   "account": { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class Integrations::Facebook::DeliveryStatus | ||||
|   def update_message_status | ||||
|     return unless conversation | ||||
|  | ||||
|     conversation.user_last_seen_at = @params.at | ||||
|     conversation.contact_last_seen_at = @params.at | ||||
|     conversation.save! | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -105,7 +105,7 @@ | ||||
|       "git add" | ||||
|     ], | ||||
|     "!(*schema).rb": [ | ||||
|       "rubocop -a", | ||||
|       "bundle exec rubocop -a", | ||||
|       "git add" | ||||
|     ], | ||||
|     "*.scss": [ | ||||
|   | ||||
| @@ -47,7 +47,7 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do | ||||
|     context 'with a conversation' do | ||||
|       it 'returns the correct conversation params' do | ||||
|         allow(Rails.configuration.dispatcher).to receive(:dispatch) | ||||
|         expect(conversation.user_last_seen_at).to eq(nil) | ||||
|         expect(conversation.contact_last_seen_at).to eq(nil) | ||||
|  | ||||
|         post '/api/v1/widget/conversations/update_last_seen', | ||||
|              headers: { 'X-Auth-Token' => token }, | ||||
| @@ -56,7 +56,7 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|  | ||||
|         expect(conversation.reload.user_last_seen_at).not_to eq(nil) | ||||
|         expect(conversation.reload.contact_last_seen_at).not_to eq(nil) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -4,9 +4,10 @@ RSpec.describe ConversationMailbox, type: :mailbox do | ||||
|   include ActionMailbox::TestHelper | ||||
|  | ||||
|   describe 'add mail as reply in a conversation' do | ||||
|     let(:agent) { create(:user, email: 'agent1@example.com') } | ||||
|     let(:account) { create(:account) } | ||||
|     let(:agent) { create(:user, email: 'agent1@example.com', account: account) } | ||||
|     let(:reply_mail) { create_inbound_email_from_fixture('reply.eml') } | ||||
|     let(:conversation) { create(:conversation, assignee: agent, inbox: create(:inbox, greeting_enabled: false)) } | ||||
|     let(:conversation) { create(:conversation, assignee: agent, inbox: create(:inbox, account: account, greeting_enabled: false), account: account) } | ||||
|     let(:described_subject) { described_class.receive reply_mail } | ||||
|     let(:serialized_attributes) { %w[text_content html_content number_of_attachments subject date to from in_reply_to cc bcc message_id] } | ||||
|  | ||||
|   | ||||
| @@ -36,4 +36,21 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :maile | ||||
|       expect(mail.to).to eq([agent.email]) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   describe 'assigned_conversation_new_message' do | ||||
|     let(:mail) { described_class.assigned_conversation_new_message(conversation, agent).deliver_now } | ||||
|  | ||||
|     it 'renders the subject' do | ||||
|       expect(mail.subject).to eq("#{agent.available_name}, New message in your assigned conversation [ID - #{conversation.display_id}].") | ||||
|     end | ||||
|  | ||||
|     it 'renders the receiver email' do | ||||
|       expect(mail.to).to eq([agent.email]) | ||||
|     end | ||||
|  | ||||
|     it 'will not send email if agent is online' do | ||||
|       ::OnlineStatusTracker.update_presence(conversation.account.id, 'User', agent.id) | ||||
|       expect(mail).to eq nil | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -14,9 +14,9 @@ RSpec.describe ConversationReplyMailer, type: :mailer do | ||||
|     end | ||||
|  | ||||
|     context 'with summary' do | ||||
|       let(:conversation) { create(:conversation, assignee: agent) } | ||||
|       let(:message) { create(:message, conversation: conversation) } | ||||
|       let(:private_message) { create(:message, content: 'This is a private message', conversation: conversation) } | ||||
|       let(:conversation) { create(:conversation, account: account, assignee: agent) } | ||||
|       let(:message) { create(:message, account: account, conversation: conversation) } | ||||
|       let(:private_message) { create(:message, account: account, content: 'This is a private message', conversation: conversation) } | ||||
|       let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now } | ||||
|  | ||||
|       it 'renders the subject' do | ||||
| @@ -31,6 +31,12 @@ RSpec.describe ConversationReplyMailer, type: :mailer do | ||||
|         expect(mail.body.decoded).not_to include(private_message.content) | ||||
|         expect(mail.body.decoded).to include(message.content) | ||||
|       end | ||||
|  | ||||
|       it 'will not send email if conversation is already viewed by contact' do | ||||
|         create(:message, message_type: 'outgoing', account: account, conversation: conversation) | ||||
|         conversation.update(contact_last_seen_at: Time.zone.now) | ||||
|         expect(mail).to eq nil | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'without assignee' do | ||||
| @@ -75,6 +81,12 @@ RSpec.describe ConversationReplyMailer, type: :mailer do | ||||
|         expect(mail.body.decoded).not_to include(message_1.content) | ||||
|         expect(mail.body.decoded).to include(message_2.content) | ||||
|       end | ||||
|  | ||||
|       it 'will not send email if conversation is already viewed by contact' do | ||||
|         create(:message, message_type: 'outgoing', account: account, conversation: conversation) | ||||
|         conversation.update(contact_last_seen_at: Time.zone.now) | ||||
|         expect(mail).to eq nil | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when custom domain and email is not enabled' do | ||||
|   | ||||
| @@ -69,7 +69,7 @@ RSpec.describe Conversation, type: :model do | ||||
|       conversation.update( | ||||
|         status: :resolved, | ||||
|         locked: true, | ||||
|         user_last_seen_at: Time.now, | ||||
|         contact_last_seen_at: Time.now, | ||||
|         assignee: new_assignee | ||||
|       ) | ||||
|     end | ||||
| @@ -317,7 +317,7 @@ RSpec.describe Conversation, type: :model do | ||||
|         timestamp: conversation.created_at.to_i, | ||||
|         can_reply: true, | ||||
|         channel: 'Channel::WebWidget', | ||||
|         user_last_seen_at: conversation.user_last_seen_at.to_i, | ||||
|         contact_last_seen_at: conversation.contact_last_seen_at.to_i, | ||||
|         agent_last_seen_at: conversation.agent_last_seen_at.to_i, | ||||
|         unread_count: 0 | ||||
|       } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ RSpec.describe Message, type: :model do | ||||
|   end | ||||
|  | ||||
|   context 'when message is created' do | ||||
|     let(:message) { build(:message) } | ||||
|     let(:message) { build(:message, account: create(:account)) } | ||||
|  | ||||
|     it 'triggers ::MessageTemplates::HookExecutionService' do | ||||
|       hook_execution_service = double | ||||
| @@ -23,10 +23,25 @@ RSpec.describe Message, type: :model do | ||||
|       expect(hook_execution_service).to have_received(:perform) | ||||
|     end | ||||
|  | ||||
|     it 'calls notify email method on after save' do | ||||
|       allow(message).to receive(:notify_via_mail).and_return(true) | ||||
|     it 'calls notify email method on after save for outgoing messages' do | ||||
|       allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true) | ||||
|       message.message_type = 'outgoing' | ||||
|       message.save! | ||||
|       expect(message).to have_received(:notify_via_mail) | ||||
|       expect(ConversationReplyEmailWorker).to have_received(:perform_in) | ||||
|     end | ||||
|  | ||||
|     it 'wont call notify email method for private notes' do | ||||
|       message.private = true | ||||
|       allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true) | ||||
|       message.save! | ||||
|       expect(ConversationReplyEmailWorker).not_to have_received(:perform_in) | ||||
|     end | ||||
|  | ||||
|     it 'wont call notify email method unless its website or email channel' do | ||||
|       message.inbox = create(:inbox, account: message.account, channel: build(:channel_api, account: message.account)) | ||||
|       allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true) | ||||
|       message.save! | ||||
|       expect(ConversationReplyEmailWorker).not_to have_received(:perform_in) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -25,7 +25,7 @@ RSpec.describe Conversations::EventDataPresenter do | ||||
|         can_reply: conversation.can_reply?, | ||||
|         channel: conversation.inbox.channel_type, | ||||
|         timestamp: conversation.created_at.to_i, | ||||
|         user_last_seen_at: conversation.user_last_seen_at.to_i, | ||||
|         contact_last_seen_at: conversation.contact_last_seen_at.to_i, | ||||
|         agent_last_seen_at: conversation.agent_last_seen_at.to_i, | ||||
|         unread_count: 0 | ||||
|       } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ properties: | ||||
|   timestamp: | ||||
|     type: string | ||||
|     description: The time at which conversation was created | ||||
|   user_last_seen_at: | ||||
|   contact_last_seen_at: | ||||
|     type: string | ||||
|   agent_last_seen_at: | ||||
|     type: agent_last_seen_at | ||||
|   | ||||
| @@ -1088,7 +1088,7 @@ | ||||
|           "type": "string", | ||||
|           "description": "The time at which conversation was created" | ||||
|         }, | ||||
|         "user_last_seen_at": { | ||||
|         "contact_last_seen_at": { | ||||
|           "type": "string" | ||||
|         }, | ||||
|         "agent_last_seen_at": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose