mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +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/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' | ||||||
|     - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' |     - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' | ||||||
| Metrics/CyclomaticComplexity: | Metrics/CyclomaticComplexity: | ||||||
|  |   Max: 7 | ||||||
|   Exclude: |   Exclude: | ||||||
|     - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' |     - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' | ||||||
| Rails/ReversibleMigration: | Rails/ReversibleMigration: | ||||||
|   | |||||||
							
								
								
									
										104
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -18,56 +18,56 @@ GEM | |||||||
|   specs: |   specs: | ||||||
|     action-cable-testing (0.6.1) |     action-cable-testing (0.6.1) | ||||||
|       actioncable (>= 5.0) |       actioncable (>= 5.0) | ||||||
|     actioncable (6.0.3.2) |     actioncable (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       nio4r (~> 2.0) |       nio4r (~> 2.0) | ||||||
|       websocket-driver (>= 0.6.1) |       websocket-driver (>= 0.6.1) | ||||||
|     actionmailbox (6.0.3.2) |     actionmailbox (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       activejob (= 6.0.3.2) |       activejob (= 6.0.3.3) | ||||||
|       activerecord (= 6.0.3.2) |       activerecord (= 6.0.3.3) | ||||||
|       activestorage (= 6.0.3.2) |       activestorage (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       mail (>= 2.7.1) |       mail (>= 2.7.1) | ||||||
|     actionmailer (6.0.3.2) |     actionmailer (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       actionview (= 6.0.3.2) |       actionview (= 6.0.3.3) | ||||||
|       activejob (= 6.0.3.2) |       activejob (= 6.0.3.3) | ||||||
|       mail (~> 2.5, >= 2.5.4) |       mail (~> 2.5, >= 2.5.4) | ||||||
|       rails-dom-testing (~> 2.0) |       rails-dom-testing (~> 2.0) | ||||||
|     actionpack (6.0.3.2) |     actionpack (6.0.3.3) | ||||||
|       actionview (= 6.0.3.2) |       actionview (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       rack (~> 2.0, >= 2.0.8) |       rack (~> 2.0, >= 2.0.8) | ||||||
|       rack-test (>= 0.6.3) |       rack-test (>= 0.6.3) | ||||||
|       rails-dom-testing (~> 2.0) |       rails-dom-testing (~> 2.0) | ||||||
|       rails-html-sanitizer (~> 1.0, >= 1.2.0) |       rails-html-sanitizer (~> 1.0, >= 1.2.0) | ||||||
|     actiontext (6.0.3.2) |     actiontext (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       activerecord (= 6.0.3.2) |       activerecord (= 6.0.3.3) | ||||||
|       activestorage (= 6.0.3.2) |       activestorage (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       nokogiri (>= 1.8.5) |       nokogiri (>= 1.8.5) | ||||||
|     actionview (6.0.3.2) |     actionview (6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       builder (~> 3.1) |       builder (~> 3.1) | ||||||
|       erubi (~> 1.4) |       erubi (~> 1.4) | ||||||
|       rails-dom-testing (~> 2.0) |       rails-dom-testing (~> 2.0) | ||||||
|       rails-html-sanitizer (~> 1.1, >= 1.2.0) |       rails-html-sanitizer (~> 1.1, >= 1.2.0) | ||||||
|     activejob (6.0.3.2) |     activejob (6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       globalid (>= 0.3.6) |       globalid (>= 0.3.6) | ||||||
|     activemodel (6.0.3.2) |     activemodel (6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|     activerecord (6.0.3.2) |     activerecord (6.0.3.3) | ||||||
|       activemodel (= 6.0.3.2) |       activemodel (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|     activestorage (6.0.3.2) |     activestorage (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       activejob (= 6.0.3.2) |       activejob (= 6.0.3.3) | ||||||
|       activerecord (= 6.0.3.2) |       activerecord (= 6.0.3.3) | ||||||
|       marcel (~> 0.3.1) |       marcel (~> 0.3.1) | ||||||
|     activesupport (6.0.3.2) |     activesupport (6.0.3.3) | ||||||
|       concurrent-ruby (~> 1.0, >= 1.0.2) |       concurrent-ruby (~> 1.0, >= 1.0.2) | ||||||
|       i18n (>= 0.7, < 2) |       i18n (>= 0.7, < 2) | ||||||
|       minitest (~> 5.1) |       minitest (~> 5.1) | ||||||
| @@ -299,7 +299,7 @@ GEM | |||||||
|     mini_magick (4.10.1) |     mini_magick (4.10.1) | ||||||
|     mini_mime (1.0.2) |     mini_mime (1.0.2) | ||||||
|     mini_portile2 (2.4.0) |     mini_portile2 (2.4.0) | ||||||
|     minitest (5.14.1) |     minitest (5.14.2) | ||||||
|     momentjs-rails (2.20.1) |     momentjs-rails (2.20.1) | ||||||
|       railties (>= 3.1) |       railties (>= 3.1) | ||||||
|     msgpack (1.3.3) |     msgpack (1.3.3) | ||||||
| @@ -307,7 +307,7 @@ GEM | |||||||
|     multi_xml (0.6.0) |     multi_xml (0.6.0) | ||||||
|     multipart-post (2.1.1) |     multipart-post (2.1.1) | ||||||
|     netrc (0.11.0) |     netrc (0.11.0) | ||||||
|     nio4r (2.5.2) |     nio4r (2.5.3) | ||||||
|     nokogiri (1.10.10) |     nokogiri (1.10.10) | ||||||
|       mini_portile2 (~> 2.4.0) |       mini_portile2 (~> 2.4.0) | ||||||
|     oauth (0.5.4) |     oauth (0.5.4) | ||||||
| @@ -336,29 +336,29 @@ GEM | |||||||
|       rack |       rack | ||||||
|     rack-test (1.1.0) |     rack-test (1.1.0) | ||||||
|       rack (>= 1.0, < 3) |       rack (>= 1.0, < 3) | ||||||
|     rails (6.0.3.2) |     rails (6.0.3.3) | ||||||
|       actioncable (= 6.0.3.2) |       actioncable (= 6.0.3.3) | ||||||
|       actionmailbox (= 6.0.3.2) |       actionmailbox (= 6.0.3.3) | ||||||
|       actionmailer (= 6.0.3.2) |       actionmailer (= 6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       actiontext (= 6.0.3.2) |       actiontext (= 6.0.3.3) | ||||||
|       actionview (= 6.0.3.2) |       actionview (= 6.0.3.3) | ||||||
|       activejob (= 6.0.3.2) |       activejob (= 6.0.3.3) | ||||||
|       activemodel (= 6.0.3.2) |       activemodel (= 6.0.3.3) | ||||||
|       activerecord (= 6.0.3.2) |       activerecord (= 6.0.3.3) | ||||||
|       activestorage (= 6.0.3.2) |       activestorage (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       bundler (>= 1.3.0) |       bundler (>= 1.3.0) | ||||||
|       railties (= 6.0.3.2) |       railties (= 6.0.3.3) | ||||||
|       sprockets-rails (>= 2.0.0) |       sprockets-rails (>= 2.0.0) | ||||||
|     rails-dom-testing (2.0.3) |     rails-dom-testing (2.0.3) | ||||||
|       activesupport (>= 4.2.0) |       activesupport (>= 4.2.0) | ||||||
|       nokogiri (>= 1.6) |       nokogiri (>= 1.6) | ||||||
|     rails-html-sanitizer (1.3.0) |     rails-html-sanitizer (1.3.0) | ||||||
|       loofah (~> 2.3) |       loofah (~> 2.3) | ||||||
|     railties (6.0.3.2) |     railties (6.0.3.3) | ||||||
|       actionpack (= 6.0.3.2) |       actionpack (= 6.0.3.3) | ||||||
|       activesupport (= 6.0.3.2) |       activesupport (= 6.0.3.3) | ||||||
|       method_source |       method_source | ||||||
|       rake (>= 0.8.7) |       rake (>= 0.8.7) | ||||||
|       thor (>= 0.20.3, < 2.0) |       thor (>= 0.20.3, < 2.0) | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController | |||||||
|   def update_last_seen |   def update_last_seen | ||||||
|     head :ok && return if conversation.nil? |     head :ok && return if conversation.nil? | ||||||
|  |  | ||||||
|     conversation.user_last_seen_at = DateTime.now.utc |     conversation.contact_last_seen_at = DateTime.now.utc | ||||||
|     conversation.save! |     conversation.save! | ||||||
|     head :ok |     head :ok | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -26,7 +26,8 @@ | |||||||
|         "TITLE": "Email Notifications", |         "TITLE": "Email Notifications", | ||||||
|         "NOTE": "Update your email notification preferences here", |         "NOTE": "Update your email notification preferences here", | ||||||
|         "CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me", |         "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": { |       "API": { | ||||||
|         "UPDATE_SUCCESS": "Your notification preferences are updated successfully", |         "UPDATE_SUCCESS": "Your notification preferences are updated successfully", | ||||||
| @@ -37,6 +38,7 @@ | |||||||
|         "NOTE": "Update your push notification preferences here", |         "NOTE": "Update your push notification preferences here", | ||||||
|         "CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me", |         "CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me", | ||||||
|         "CONVERSATION_CREATION": "Send push notifications when a new conversation is created", |         "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.", |         "HAS_ENABLED_PUSH": "You have enabled push for this browser.", | ||||||
|         "REQUEST_PUSH": "Enable push notifications" |         "REQUEST_PUSH": "Enable push notifications" | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -43,6 +43,23 @@ | |||||||
|             }} |             }} | ||||||
|           </label> |           </label> | ||||||
|         </div> |         </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> |     </div> | ||||||
|     <div v-if="vapidPublicKey" class="profile--settings--row row push-row"> |     <div v-if="vapidPublicKey" class="profile--settings--row row push-row"> | ||||||
| @@ -105,6 +122,23 @@ | |||||||
|             }} |             }} | ||||||
|           </label> |           </label> | ||||||
|         </div> |         </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> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ export default [ | |||||||
|     inbox_id: 1, |     inbox_id: 1, | ||||||
|     status: 0, |     status: 0, | ||||||
|     timestamp: 1578555084, |     timestamp: 1578555084, | ||||||
|     user_last_seen_at: 0, |     contact_last_seen_at: 0, | ||||||
|     agent_last_seen_at: 1578555084, |     agent_last_seen_at: 1578555084, | ||||||
|     unread_count: 0, |     unread_count: 0, | ||||||
|   }, |   }, | ||||||
| @@ -75,7 +75,7 @@ export default [ | |||||||
|     inbox_id: 2, |     inbox_id: 2, | ||||||
|     status: 0, |     status: 0, | ||||||
|     timestamp: 1578555084, |     timestamp: 1578555084, | ||||||
|     user_last_seen_at: 0, |     contact_last_seen_at: 0, | ||||||
|     agent_last_seen_at: 1578555084, |     agent_last_seen_at: 1578555084, | ||||||
|     unread_count: 0, |     unread_count: 0, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ const toggleTyping = async ({ typingStatus }) => { | |||||||
| const setUserLastSeenAt = async ({ lastSeen }) => { | const setUserLastSeenAt = async ({ lastSeen }) => { | ||||||
|   return API.post( |   return API.post( | ||||||
|     `/api/v1/widget/conversations/update_last_seen${window.location.search}`, |     `/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 }) => { |   get: async ({ commit }) => { | ||||||
|     try { |     try { | ||||||
|       const { data } = await getConversationAPI(); |       const { data } = await getConversationAPI(); | ||||||
|       const { user_last_seen_at: lastSeen } = data; |       const { contact_last_seen_at: lastSeen } = data; | ||||||
|       commit(SET_CONVERSATION_ATTRIBUTES, data); |       commit(SET_CONVERSATION_ATTRIBUTES, data); | ||||||
|       commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); |       commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ class ContactAvatarJob < ApplicationJob | |||||||
|   def perform(contact, avatar_url) |   def perform(contact, avatar_url) | ||||||
|     avatar_resource = LocalResource.new(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) |     contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding) | ||||||
|   rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError => e |   rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED, SocketError, NoMethodError => e | ||||||
|     Rails.logger.info "invalid url #{file_url} : #{e.message}" |     Rails.logger.info "invalid url #{avatar_url} : #{e.message}" | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -2,6 +2,9 @@ class Notification::EmailNotificationJob < ApplicationJob | |||||||
|   queue_as :default |   queue_as :default | ||||||
|  |  | ||||||
|   def perform(notification) |   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 |     Notification::EmailNotificationService.new(notification: notification).perform | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -26,4 +26,20 @@ class NotificationListener < BaseListener | |||||||
|       primary_actor: conversation |       primary_actor: conversation | ||||||
|     ).perform |     ).perform | ||||||
|   end |   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 | end | ||||||
|   | |||||||
| @@ -19,6 +19,18 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer | |||||||
|     send_mail_with_liquid(to: @agent.email, subject: subject) and return |     send_mail_with_liquid(to: @agent.email, subject: subject) and return | ||||||
|   end |   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 |   private | ||||||
|  |  | ||||||
|   def liquid_droppables |   def liquid_droppables | ||||||
|   | |||||||
| @@ -50,7 +50,13 @@ class ApplicationMailer < ActionMailer::Base | |||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def locale_from_account(account) | ||||||
|  |     I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def ensure_current_account(account) |   def ensure_current_account(account) | ||||||
|     Current.account = account if account.present? |     Current.account = account if account.present? | ||||||
|  |     locale ||= locale_from_account(account) if account.present? | ||||||
|  |     I18n.locale = locale || I18n.default_locale | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ class ConversationReplyMailer < ApplicationMailer | |||||||
|     return unless smtp_config_set_or_development? |     return unless smtp_config_set_or_development? | ||||||
|  |  | ||||||
|     init_conversation_attributes(conversation) |     init_conversation_attributes(conversation) | ||||||
|  |     return if conversation_already_viewed? | ||||||
|  |  | ||||||
|     recap_messages = @conversation.messages.chat.where('created_at < ?', message_queued_time).last(10) |     recap_messages = @conversation.messages.chat.where('created_at < ?', message_queued_time).last(10) | ||||||
|     new_messages = @conversation.messages.chat.where('created_at >= ?', message_queued_time) |     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? |     return unless smtp_config_set_or_development? | ||||||
|  |  | ||||||
|     init_conversation_attributes(conversation) |     init_conversation_attributes(conversation) | ||||||
|  |     return if conversation_already_viewed? | ||||||
|  |  | ||||||
|     @messages = @conversation.messages.chat.outgoing.where('created_at >= ?', message_queued_time) |     @messages = @conversation.messages.chat.outgoing.where('created_at >= ?', message_queued_time) | ||||||
|     return false if @messages.count.zero? |     return false if @messages.count.zero? | ||||||
| @@ -63,6 +65,18 @@ class ConversationReplyMailer < ApplicationMailer | |||||||
|     @agent = @conversation.assignee |     @agent = @conversation.assignee | ||||||
|   end |   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 |   def assignee_name | ||||||
|     @assignee_name ||= @agent&.available_name || 'Notifications' |     @assignee_name ||= @agent&.available_name || 'Notifications' | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -5,10 +5,10 @@ | |||||||
| #  id                    :integer          not null, primary key | #  id                    :integer          not null, primary key | ||||||
| #  additional_attributes :jsonb | #  additional_attributes :jsonb | ||||||
| #  agent_last_seen_at    :datetime | #  agent_last_seen_at    :datetime | ||||||
|  | #  contact_last_seen_at  :datetime | ||||||
| #  identifier            :string | #  identifier            :string | ||||||
| #  locked                :boolean          default(FALSE) | #  locked                :boolean          default(FALSE) | ||||||
| #  status                :integer          default("open"), not null | #  status                :integer          default("open"), not null | ||||||
| #  user_last_seen_at     :datetime |  | ||||||
| #  uuid                  :uuid             not null | #  uuid                  :uuid             not null | ||||||
| #  created_at            :datetime         not null | #  created_at            :datetime         not null | ||||||
| #  updated_at            :datetime         not null | #  updated_at            :datetime         not null | ||||||
| @@ -166,7 +166,7 @@ class Conversation < ApplicationRecord | |||||||
|     { |     { | ||||||
|       CONVERSATION_OPENED => -> { saved_change_to_status? && open? }, |       CONVERSATION_OPENED => -> { saved_change_to_status? && open? }, | ||||||
|       CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? }, |       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? }, |       CONVERSATION_LOCK_TOGGLE => -> { saved_change_to_locked? }, | ||||||
|       ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }, |       ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }, | ||||||
|       CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? } |       CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? } | ||||||
|   | |||||||
| @@ -150,14 +150,28 @@ class Message < ApplicationRecord | |||||||
|     ::MessageTemplates::HookExecutionService.new(message: self).perform |     ::MessageTemplates::HookExecutionService.new(message: self).perform | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def notify_via_mail |   def email_notifiable_message? | ||||||
|     if Redis::Alfred.get(conversation_mail_key).nil? && conversation.contact.email? && outgoing? && !private |     return false unless outgoing? | ||||||
|       # set a redis key for the conversation so that we don't need to send email for every |     return false if private? | ||||||
|       # 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) |  | ||||||
|  |  | ||||||
|       # Since this is live chat, send the email after few minutes so the only one email with |     true | ||||||
|       # last few messages coupled together is sent rather than email for each message |   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) |       ConversationReplyEmailWorker.perform_in(2.minutes, conversation.id, Time.zone.now) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -31,7 +31,8 @@ class Notification < ApplicationRecord | |||||||
|  |  | ||||||
|   NOTIFICATION_TYPES = { |   NOTIFICATION_TYPES = { | ||||||
|     conversation_creation: 1, |     conversation_creation: 1, | ||||||
|     conversation_assignment: 2 |     conversation_assignment: 2, | ||||||
|  |     assigned_conversation_new_message: 3 | ||||||
|   }.freeze |   }.freeze | ||||||
|  |  | ||||||
|   enum notification_type: NOTIFICATION_TYPES |   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 "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 |   end | ||||||
|  |  | ||||||
| @@ -71,6 +74,7 @@ class Notification < ApplicationRecord | |||||||
|  |  | ||||||
|   def process_notification_delivery |   def process_notification_delivery | ||||||
|     Notification::PushNotificationJob.perform_later(self) |     Notification::PushNotificationJob.perform_later(self) | ||||||
|  |  | ||||||
|     # Should we do something about the case where user subscribed to both push and email ? |     # 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 |     # 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. |     # 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 |   def push_timestamps | ||||||
|     { |     { | ||||||
|       agent_last_seen_at: agent_last_seen_at.to_i, |       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 |       timestamp: created_at.to_i | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -64,6 +64,8 @@ class Notification::PushNotificationService | |||||||
|     ) |     ) | ||||||
|   rescue Webpush::ExpiredSubscription |   rescue Webpush::ExpiredSubscription | ||||||
|     subscription.destroy! |     subscription.destroy! | ||||||
|  |   rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e | ||||||
|  |     Rails.logger.info "Webpush operation error: #{e.message}" | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def send_fcm_push(subscription) |   def send_fcm_push(subscription) | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ json.status conversation.status | |||||||
| json.muted conversation.muted? | json.muted conversation.muted? | ||||||
| json.can_reply conversation.can_reply? | json.can_reply conversation.can_reply? | ||||||
| json.timestamp conversation.messages.last.try(:created_at).try(:to_i) | 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.agent_last_seen_at conversation.agent_last_seen_at.to_i | ||||||
| json.unread_count conversation.unread_incoming_messages.count | json.unread_count conversation.unread_incoming_messages.count | ||||||
| json.additional_attributes conversation.additional_attributes | json.additional_attributes conversation.additional_attributes | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| if @conversation | if @conversation | ||||||
|   json.id @conversation.display_id |   json.id @conversation.display_id | ||||||
|   json.inbox_id @conversation.inbox_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 |   json.status @conversation.status | ||||||
| end | 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. | # 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 |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "pgcrypto" |   enable_extension "pgcrypto" | ||||||
| @@ -219,7 +219,7 @@ ActiveRecord::Schema.define(version: 2020_08_28_175931) do | |||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|     t.bigint "contact_id" |     t.bigint "contact_id" | ||||||
|     t.integer "display_id", null: false |     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.datetime "agent_last_seen_at" | ||||||
|     t.boolean "locked", default: false |     t.boolean "locked", default: false | ||||||
|     t.jsonb "additional_attributes" |     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, |     "inbox_id": 0, | ||||||
|     "status": "open", |     "status": "open", | ||||||
|     "agent_last_seen_at": 0, |     "agent_last_seen_at": 0, | ||||||
|     "user_last_seen_at": 0, |     "contact_last_seen_at": 0, | ||||||
|     "timestamp": 0 |     "timestamp": 0 | ||||||
|   }, |   }, | ||||||
|   "account": { |   "account": { | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ class Integrations::Facebook::DeliveryStatus | |||||||
|   def update_message_status |   def update_message_status | ||||||
|     return unless conversation |     return unless conversation | ||||||
|  |  | ||||||
|     conversation.user_last_seen_at = @params.at |     conversation.contact_last_seen_at = @params.at | ||||||
|     conversation.save! |     conversation.save! | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ | |||||||
|       "git add" |       "git add" | ||||||
|     ], |     ], | ||||||
|     "!(*schema).rb": [ |     "!(*schema).rb": [ | ||||||
|       "rubocop -a", |       "bundle exec rubocop -a", | ||||||
|       "git add" |       "git add" | ||||||
|     ], |     ], | ||||||
|     "*.scss": [ |     "*.scss": [ | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do | |||||||
|     context 'with a conversation' do |     context 'with a conversation' do | ||||||
|       it 'returns the correct conversation params' do |       it 'returns the correct conversation params' do | ||||||
|         allow(Rails.configuration.dispatcher).to receive(:dispatch) |         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', |         post '/api/v1/widget/conversations/update_last_seen', | ||||||
|              headers: { 'X-Auth-Token' => token }, |              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(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 |     end | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -4,9 +4,10 @@ RSpec.describe ConversationMailbox, type: :mailbox do | |||||||
|   include ActionMailbox::TestHelper |   include ActionMailbox::TestHelper | ||||||
|  |  | ||||||
|   describe 'add mail as reply in a conversation' do |   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(: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(: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] } |     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]) |       expect(mail.to).to eq([agent.email]) | ||||||
|     end |     end | ||||||
|   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 | end | ||||||
|   | |||||||
| @@ -14,9 +14,9 @@ RSpec.describe ConversationReplyMailer, type: :mailer do | |||||||
|     end |     end | ||||||
|  |  | ||||||
|     context 'with summary' do |     context 'with summary' do | ||||||
|       let(:conversation) { create(:conversation, assignee: agent) } |       let(:conversation) { create(:conversation, account: account, assignee: agent) } | ||||||
|       let(:message) { create(:message, conversation: conversation) } |       let(:message) { create(:message, account: account, conversation: conversation) } | ||||||
|       let(:private_message) { create(:message, content: 'This is a private message', 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 } |       let(:mail) { described_class.reply_with_summary(message.conversation, Time.zone.now).deliver_now } | ||||||
|  |  | ||||||
|       it 'renders the subject' do |       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).not_to include(private_message.content) | ||||||
|         expect(mail.body.decoded).to include(message.content) |         expect(mail.body.decoded).to include(message.content) | ||||||
|       end |       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 |     end | ||||||
|  |  | ||||||
|     context 'without assignee' do |     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).not_to include(message_1.content) | ||||||
|         expect(mail.body.decoded).to include(message_2.content) |         expect(mail.body.decoded).to include(message_2.content) | ||||||
|       end |       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 |     end | ||||||
|  |  | ||||||
|     context 'when custom domain and email is not enabled' do |     context 'when custom domain and email is not enabled' do | ||||||
|   | |||||||
| @@ -69,7 +69,7 @@ RSpec.describe Conversation, type: :model do | |||||||
|       conversation.update( |       conversation.update( | ||||||
|         status: :resolved, |         status: :resolved, | ||||||
|         locked: true, |         locked: true, | ||||||
|         user_last_seen_at: Time.now, |         contact_last_seen_at: Time.now, | ||||||
|         assignee: new_assignee |         assignee: new_assignee | ||||||
|       ) |       ) | ||||||
|     end |     end | ||||||
| @@ -317,7 +317,7 @@ RSpec.describe Conversation, type: :model do | |||||||
|         timestamp: conversation.created_at.to_i, |         timestamp: conversation.created_at.to_i, | ||||||
|         can_reply: true, |         can_reply: true, | ||||||
|         channel: 'Channel::WebWidget', |         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, |         agent_last_seen_at: conversation.agent_last_seen_at.to_i, | ||||||
|         unread_count: 0 |         unread_count: 0 | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ RSpec.describe Message, type: :model do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   context 'when message is created' do |   context 'when message is created' do | ||||||
|     let(:message) { build(:message) } |     let(:message) { build(:message, account: create(:account)) } | ||||||
|  |  | ||||||
|     it 'triggers ::MessageTemplates::HookExecutionService' do |     it 'triggers ::MessageTemplates::HookExecutionService' do | ||||||
|       hook_execution_service = double |       hook_execution_service = double | ||||||
| @@ -23,10 +23,25 @@ RSpec.describe Message, type: :model do | |||||||
|       expect(hook_execution_service).to have_received(:perform) |       expect(hook_execution_service).to have_received(:perform) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     it 'calls notify email method on after save' do |     it 'calls notify email method on after save for outgoing messages' do | ||||||
|       allow(message).to receive(:notify_via_mail).and_return(true) |       allow(ConversationReplyEmailWorker).to receive(:perform_in).and_return(true) | ||||||
|  |       message.message_type = 'outgoing' | ||||||
|       message.save! |       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 |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ RSpec.describe Conversations::EventDataPresenter do | |||||||
|         can_reply: conversation.can_reply?, |         can_reply: conversation.can_reply?, | ||||||
|         channel: conversation.inbox.channel_type, |         channel: conversation.inbox.channel_type, | ||||||
|         timestamp: conversation.created_at.to_i, |         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, |         agent_last_seen_at: conversation.agent_last_seen_at.to_i, | ||||||
|         unread_count: 0 |         unread_count: 0 | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ properties: | |||||||
|   timestamp: |   timestamp: | ||||||
|     type: string |     type: string | ||||||
|     description: The time at which conversation was created |     description: The time at which conversation was created | ||||||
|   user_last_seen_at: |   contact_last_seen_at: | ||||||
|     type: string |     type: string | ||||||
|   agent_last_seen_at: |   agent_last_seen_at: | ||||||
|     type: agent_last_seen_at |     type: agent_last_seen_at | ||||||
|   | |||||||
| @@ -1088,7 +1088,7 @@ | |||||||
|           "type": "string", |           "type": "string", | ||||||
|           "description": "The time at which conversation was created" |           "description": "The time at which conversation was created" | ||||||
|         }, |         }, | ||||||
|         "user_last_seen_at": { |         "contact_last_seen_at": { | ||||||
|           "type": "string" |           "type": "string" | ||||||
|         }, |         }, | ||||||
|         "agent_last_seen_at": { |         "agent_last_seen_at": { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose