diff --git a/Gemfile b/Gemfile index 40fd33957..a40b0f816 100644 --- a/Gemfile +++ b/Gemfile @@ -109,9 +109,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.12.0', require: false +gem 'sentry-rails', '>= 5.13.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.12.0', require: false +gem 'sentry-sidekiq', '>= 5.13.0', require: false ##-- background job processing --## gem 'sidekiq', '>= 7.1.3' @@ -200,6 +200,8 @@ group :development do # profiling gem 'rack-mini-profiler', '>= 3.1.1', require: false gem 'stackprof' + # Should install the associated chrome extension to view query logs + gem 'meta_request' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index a6390908b..8c782dd48 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -451,13 +451,16 @@ GEM marcel (1.0.2) maxminddb (0.1.22) memoist (0.16.2) + meta_request (0.7.4) + rack-contrib (>= 1.1, < 3) + railties (>= 3.0.0, < 7.1) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.4) + mini_portile2 (2.8.5) minitest (5.20.0) mock_redis (0.36.0) ruby2_keywords @@ -555,10 +558,12 @@ GEM pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.1) + racc (1.7.3) rack (2.2.8) rack-attack (6.7.0) rack (>= 1.0, < 4) + rack-contrib (2.4.0) + rack (< 4) rack-cors (2.0.1) rack (>= 2.0.0) rack-mini-profiler (3.1.1) @@ -599,7 +604,7 @@ GEM thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -704,13 +709,13 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.12.0) + sentry-rails (5.13.0) railties (>= 5.0) - sentry-ruby (~> 5.12.0) - sentry-ruby (5.12.0) + sentry-ruby (~> 5.13.0) + sentry-ruby (5.13.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.12.0) - sentry-ruby (~> 5.12.0) + sentry-sidekiq (5.13.0) + sentry-ruby (~> 5.13.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) @@ -760,7 +765,7 @@ GEM stripe (8.5.0) telephone_number (1.4.20) test-prof (1.2.1) - thor (1.2.2) + thor (1.3.0) tilt (2.2.0) time_diff (0.3.0) activesupport @@ -895,6 +900,7 @@ DEPENDENCIES listen lograge (~> 0.14.0) maxminddb + meta_request mock_redis neighbor newrelic-sidekiq-metrics (>= 1.6.2) @@ -929,9 +935,9 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.12.0) + sentry-rails (>= 5.13.0) sentry-ruby - sentry-sidekiq (>= 5.12.0) + sentry-sidekiq (>= 5.13.0) shoulda-matchers sidekiq (>= 7.1.3) sidekiq-cron (>= 1.11.0) diff --git a/README.md b/README.md index ac11d649d..11ac5dc2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -
-
+
+
-
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
- +___ -+# Chatwoot + +Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc. +
-___ - -+
-
-
-
+
+
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index 6d2a181f0..bd8da5549 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok
end
+ def logo
+ @portal.logo.purge if @portal.logo.attached?
+ head :ok
+ end
+
def process_attached_logo
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id)
diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js
index 28220b0b5..7c6210dbd 100644
--- a/app/javascript/dashboard/api/helpCenter/portals.js
+++ b/app/javascript/dashboard/api/helpCenter/portals.js
@@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`);
}
+
+ deleteLogo(portalSlug) {
+ return axios.delete(`${this.url}/${portalSlug}/logo`);
+ }
}
export default PortalsAPI;
diff --git a/app/javascript/dashboard/assets/scss/_formulate.scss b/app/javascript/dashboard/assets/scss/_formulate.scss
index 57c43a3d6..d683ce3c7 100644
--- a/app/javascript/dashboard/assets/scss/_formulate.scss
+++ b/app/javascript/dashboard/assets/scss/_formulate.scss
@@ -16,3 +16,23 @@
width: 100%;
}
}
+
+.integration-hooks {
+ .formulate-input[data-type='checkbox'] {
+ .formulate-input-wrapper {
+ @apply flex;
+
+ .formulate-input-element {
+ @apply pr-2;
+
+ input {
+ @apply mb-0;
+ }
+ }
+ }
+
+ .formulate-input-element-decorator {
+ @apply hidden;
+ }
+ }
+}
diff --git a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
index ab4047a1e..2d7951b05 100644
--- a/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
+++ b/app/javascript/dashboard/components/widgets/AIAssistanceButton.vue
@@ -61,6 +61,7 @@ export default {
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
+ isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}),
isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed;
@@ -70,7 +71,8 @@ export default {
return (
this.isAdmin &&
!this.isAIIntegrationEnabled &&
- !this.isAICTAModalDismissed
+ !this.isAICTAModalDismissed &&
+ this.isAChatwootInstance
);
},
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
index 38bb24f8f..10b1dce32 100644
--- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
@@ -114,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
-import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -285,14 +284,6 @@ export default {
return this.currentChat.unread_count || 0;
},
inboxSupportsReplyTo() {
- if (
- !this.isFeatureEnabledonAccount(
- this.accountId,
- FEATURE_FLAGS.MESSAGE_REPLY_TO
- )
- ) {
- return {};
- }
return {
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
@@ -349,7 +340,7 @@ export default {
// method available in mixin, need to ensure that integrations are present
await this.fetchIntegrationsIfRequired();
- if (!this.isAIIntegrationEnabled) {
+ if (!this.isLabelSuggestionFeatureEnabled) {
return;
}
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 37c34a031..5e1f34904 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -194,7 +194,6 @@ import {
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
-import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -276,11 +275,7 @@ export default {
return (
this.inReplyTo?.id &&
!this.isPrivate &&
- this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
- this.isFeatureEnabledonAccount(
- this.accountId,
- FEATURE_FLAGS.MESSAGE_REPLY_TO
- )
+ this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
);
},
showRichContentEditor() {
diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
index 4ee7aaa9d..416af5d06 100644
--- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
@@ -192,7 +192,7 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
-
+ // Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
@@ -201,11 +201,12 @@ export default {
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox ||
- this.isASmsInbox
+ this.isASmsInbox ||
+ this.isATelegramChannel
) {
return this.sourceId && this.isSent;
}
- // There is no source id for the line channel
+ // All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
@@ -216,15 +217,15 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
-
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
- this.isASmsInbox
+ this.isASmsInbox ||
+ this.isAFacebookInbox
) {
return this.sourceId && this.isDelivered;
}
- // We will consider messages as delivered for web widget inbox and API inbox if they are sent
+ // All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent;
}
@@ -238,12 +239,6 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
-
- if (this.isAWebWidgetInbox || this.isAPIInbox) {
- const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
- return contactLastSeenAt >= this.createdAt;
- }
-
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
@@ -252,6 +247,10 @@ export default {
return this.sourceId && this.isRead;
}
+ if (this.isAWebWidgetInbox || this.isAPIInbox) {
+ return this.isRead;
+ }
+
return false;
},
},
diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js
index 143ee521d..4280e0f10 100644
--- a/app/javascript/dashboard/featureFlags.js
+++ b/app/javascript/dashboard/featureFlags.js
@@ -16,6 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
- MESSAGE_REPLY_TO: 'message_reply_to',
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
};
diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js
index 7ea8531d3..5464d1107 100644
--- a/app/javascript/dashboard/helper/URLHelper.js
+++ b/app/javascript/dashboard/helper/URLHelper.js
@@ -96,3 +96,15 @@ export const getArticleSearchURL = ({
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
};
+
+export const hasValidAvatarUrl = avatarUrl => {
+ try {
+ const { host: avatarUrlHost } = new URL(avatarUrl);
+ const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
+ avatarUrlHost
+ );
+ return avatarUrl && !isFromGravatar;
+ } catch (error) {
+ return false;
+ }
+};
diff --git a/app/javascript/dashboard/helper/automationHelper.js b/app/javascript/dashboard/helper/automationHelper.js
index f186b0e92..d1a85570b 100644
--- a/app/javascript/dashboard/helper/automationHelper.js
+++ b/app/javascript/dashboard/helper/automationHelper.js
@@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
+ remove_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES,
};
return actionsMap[type];
diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
index 1b5814556..d137c367c 100644
--- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js
+++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
@@ -4,6 +4,7 @@ import {
isValidURL,
conversationListPageURL,
getArticleSearchURL,
+ hasValidAvatarUrl,
} from '../URLHelper';
describe('#URL Helpers', () => {
@@ -164,4 +165,29 @@ describe('#URL Helpers', () => {
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
});
+
+ describe('hasValidAvatarUrl', () => {
+ test('should return true for valid non-Gravatar URL', () => {
+ expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
+ });
+
+ test('should return false for a Gravatar URL (www.gravatar.com)', () => {
+ expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
+ false
+ );
+ });
+
+ test('should return false for a Gravatar URL (gravatar)', () => {
+ expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
+ });
+
+ test('should handle invalid URL', () => {
+ expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
+ });
+
+ test('should return false for empty or undefined URL', () => {
+ expect(hasValidAvatarUrl('')).toBe(false);
+ expect(hasValidAvatarUrl()).toBe(false);
+ });
+ });
});
diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
index 43b419328..6d4a3eb8c 100644
--- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json
+++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
@@ -223,7 +223,10 @@
"LOGO": {
"LABEL": "Logo",
"UPLOAD_BUTTON": "Upload logo",
- "HELP_TEXT": "This logo will be displayed on the portal header."
+ "HELP_TEXT": "This logo will be displayed on the portal header.",
+ "IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
+ "IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
+ "IMAGE_DELETE_ERROR": "Error while deleting logo"
},
"NAME": {
"LABEL": "Name",
diff --git a/app/javascript/dashboard/mixins/aiMixin.js b/app/javascript/dashboard/mixins/aiMixin.js
index 3a9fc4a75..54bde3ffb 100644
--- a/app/javascript/dashboard/mixins/aiMixin.js
+++ b/app/javascript/dashboard/mixins/aiMixin.js
@@ -15,18 +15,26 @@ export default {
currentChat: 'getSelectedChat',
replyMode: 'draftMessages/getReplyEditorMode',
}),
- isAIIntegrationEnabled() {
- return !!this.appIntegrations.find(
+ aiIntegration() {
+ return this.appIntegrations.find(
integration => integration.id === 'openai' && !!integration.hooks.length
- );
+ )?.hooks[0];
+ },
+ isAIIntegrationEnabled() {
+ return !!this.aiIntegration;
+ },
+ isLabelSuggestionFeatureEnabled() {
+ if (this.aiIntegration) {
+ const { settings = {} } = this.aiIntegration || {};
+ return settings.label_suggestion;
+ }
+ return false;
},
isFetchingAppIntegrations() {
return this.uiFlags.isFetching;
},
hookId() {
- return this.appIntegrations.find(
- integration => integration.id === 'openai' && !!integration.hooks.length
- ).hooks[0].id;
+ return this.aiIntegration.id;
},
draftMessage() {
return this.$store.getters['draftMessages/get'](this.draftKey);
diff --git a/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue b/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue
index 64355b6cd..b5d4bbc28 100644
--- a/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue
+++ b/app/javascript/dashboard/modules/widget-preview/components/WidgetFooter.vue
@@ -12,6 +12,7 @@