mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +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