diff --git a/.codeclimate.yml b/.codeclimate.yml index af0c0714f..f26021240 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,4 +1,4 @@ -version: "2" +version: '2' plugins: rubocop: enabled: false @@ -17,30 +17,30 @@ checks: method-count: enabled: true config: - threshold: 30 + threshold: 32 file-lines: enabled: true config: threshold: 300 exclude_patterns: - - "spec/" - - "**/specs/" - - "db/*" - - "bin/**/*" - - "db/**/*" - - "config/**/*" - - "public/**/*" - - "vendor/**/*" - - "node_modules/**/*" - - "lib/tasks/auto_annotate_models.rake" - - "app/test-matchers.js" - - "docs/*" - - "**/*.md" - - "**/*.yml" - - "app/javascript/dashboard/i18n/locale" - - "**/*.stories.js" - - "stories/" - - "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js" - - "app/javascript/shared/constants/countries.js" - - "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js" - - "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js" + - 'spec/' + - '**/specs/' + - 'db/*' + - 'bin/**/*' + - 'db/**/*' + - 'config/**/*' + - 'public/**/*' + - 'vendor/**/*' + - 'node_modules/**/*' + - 'lib/tasks/auto_annotate_models.rake' + - 'app/test-matchers.js' + - 'docs/*' + - '**/*.md' + - '**/*.yml' + - 'app/javascript/dashboard/i18n/locale' + - '**/*.stories.js' + - 'stories/' + - 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js' + - 'app/javascript/shared/constants/countries.js' + - 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js' + - 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js' diff --git a/.rubocop.yml b/.rubocop.yml index 8f8473547..e9964b1c0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -87,6 +87,7 @@ Naming/VariableNumber: Metrics/MethodLength: Exclude: - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' + - 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb' Rails/CreateTableWithTimestamps: Exclude: - 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb' @@ -102,6 +103,7 @@ Metrics/AbcSize: - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' - 'app/controllers/api/v1/accounts/inboxes_controller.rb' + - 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb' Metrics/CyclomaticComplexity: Max: 7 Exclude: diff --git a/Gemfile b/Gemfile index bca6463d5..f3fbce89d 100644 --- a/Gemfile +++ b/Gemfile @@ -121,6 +121,10 @@ gem 'hairtrigger' gem 'procore-sift' +# parse email +gem 'email_reply_trimmer' +gem 'html2text' + group :production, :staging do # we dont want request timing out in development while using byebug gem 'rack-timeout' diff --git a/Gemfile.lock b/Gemfile.lock index b319c9103..b61ef372c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + actioncable (6.1.4.3) + actionpack (= 6.1.4.3) + activesupport (= 6.1.4.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailbox (6.1.4.3) + actionpack (= 6.1.4.3) + activejob (= 6.1.4.3) + activerecord (= 6.1.4.3) + activestorage (= 6.1.4.3) + activesupport (= 6.1.4.3) mail (>= 2.7.1) - actionmailer (6.1.4.1) - actionpack (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailer (6.1.4.3) + actionpack (= 6.1.4.3) + actionview (= 6.1.4.3) + activejob (= 6.1.4.3) + activesupport (= 6.1.4.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.1) - actionview (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionpack (6.1.4.3) + actionview (= 6.1.4.3) + activesupport (= 6.1.4.3) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.1) - actionpack (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actiontext (6.1.4.3) + actionpack (= 6.1.4.3) + activerecord (= 6.1.4.3) + activestorage (= 6.1.4.3) + activesupport (= 6.1.4.3) nokogiri (>= 1.8.5) - actionview (6.1.4.1) - activesupport (= 6.1.4.1) + actionview (6.1.4.3) + activesupport (= 6.1.4.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.4.1) - activesupport (= 6.1.4.1) + activejob (6.1.4.3) + activesupport (= 6.1.4.3) globalid (>= 0.3.6) - activemodel (6.1.4.1) - activesupport (= 6.1.4.1) - activerecord (6.1.4.1) - activemodel (= 6.1.4.1) - activesupport (= 6.1.4.1) + activemodel (6.1.4.3) + activesupport (= 6.1.4.3) + activerecord (6.1.4.3) + activemodel (= 6.1.4.3) + activesupport (= 6.1.4.3) activerecord-import (1.2.0) activerecord (>= 3.2) - activestorage (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activesupport (= 6.1.4.1) + activestorage (6.1.4.3) + actionpack (= 6.1.4.3) + activejob (= 6.1.4.3) + activerecord (= 6.1.4.3) + activesupport (= 6.1.4.3) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.1) + activesupport (6.1.4.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -179,6 +179,7 @@ GEM addressable (~> 2.8) ecma-re-validator (0.3.0) regexp_parser (~> 2.0) + email_reply_trimmer (0.1.13) erubi (1.10.0) erubis (2.7.0) et-orbi (1.2.5) @@ -219,7 +220,7 @@ GEM grpc (~> 1.25) geocoder (1.7.0) gli (2.20.1) - globalid (0.5.2) + globalid (1.0.0) activesupport (>= 5.0) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -290,6 +291,8 @@ GEM hashdiff (1.0.1) hashie (4.1.0) hkdf (0.3.0) + html2text (0.2.1) + nokogiri (~> 1.6) http-accept (1.7.0) http-cookie (1.0.4) domain_name (~> 0.5) @@ -297,7 +300,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.8.10) + i18n (1.8.11) concurrent-ruby (~> 1.0) image_processing (1.12.1) mini_magick (>= 4.9.5, < 5) @@ -343,7 +346,7 @@ GEM listen (3.7.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.12.0) + loofah (2.13.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -358,7 +361,7 @@ GEM mini_magick (4.11.0) mini_mime (1.1.2) mini_portile2 (2.5.3) - minitest (5.14.4) + minitest (5.15.0) mock_redis (0.29.0) ruby2_keywords momentjs-rails (2.20.1) @@ -402,7 +405,7 @@ GEM pundit (2.1.1) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.5.2) + racc (1.6.0) rack (2.2.3) rack-attack (6.5.0) rack (>= 1.0, < 3) @@ -413,29 +416,29 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.1) - actioncable (= 6.1.4.1) - actionmailbox (= 6.1.4.1) - actionmailer (= 6.1.4.1) - actionpack (= 6.1.4.1) - actiontext (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activemodel (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + rails (6.1.4.3) + actioncable (= 6.1.4.3) + actionmailbox (= 6.1.4.3) + actionmailer (= 6.1.4.3) + actionpack (= 6.1.4.3) + actiontext (= 6.1.4.3) + actionview (= 6.1.4.3) + activejob (= 6.1.4.3) + activemodel (= 6.1.4.3) + activerecord (= 6.1.4.3) + activestorage (= 6.1.4.3) + activesupport (= 6.1.4.3) bundler (>= 1.15.0) - railties (= 6.1.4.1) + railties (= 6.1.4.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + railties (6.1.4.3) + actionpack (= 6.1.4.3) + activesupport (= 6.1.4.3) method_source rake (>= 0.13) thor (~> 1.0) @@ -573,9 +576,9 @@ GEM sprockets (4.0.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) squasher (0.6.2) statsd-ruby (1.5.0) @@ -632,7 +635,7 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.0) - zeitwerk (2.4.2) + zeitwerk (2.5.1) PLATFORMS arm64-darwin-20 @@ -668,6 +671,7 @@ DEPENDENCIES devise_token_auth dotenv-rails down (~> 5.0) + email_reply_trimmer facebook-messenger factory_bot_rails faker @@ -682,6 +686,7 @@ DEPENDENCIES haikunator hairtrigger hashie + html2text image_processing jbuilder json_refs diff --git a/README.md b/README.md index eb083bec3..b2e4ccf4f 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,6 @@ ___ CircleCI Badge Docker Pull Badge Docker Build Badge - License Commits-per-month Discord @@ -29,34 +28,39 @@ ___ response time

-Chat dashboard +Chat dashboard -Chatwoot is an open-source omnichannel customer support software. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it open-source, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community. -Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community. +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. + ## Features -Chatwoot gives an integrated view of conversations happening in different communication channels. - -It supports the following conversation channels: +Chatwoot supports the following conversation channels: - **Website**: Talk to your customers using our live chat widget and make use of our SDK to identify a user and provide contextual support. - **Facebook**: Connect your Facebook pages and start replying to the direct messages to your page. + - **Instagram**: Connect your Instagram profile and start replying to the direct messages. - **Twitter**: Connect your Twitter profiles and reply to direct messages or the tweets where you are mentioned. - - **Whatsapp**: Connect your Whatsapp business account and manage the conversation in Chatwoot - - **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot + - **Telegram**: Connect your Telegram bot and reply to your customers right from a single dashboard. + - **WhatsApp**: Connect your WhatsApp business account and manage the conversation in Chatwoot. + - **Line**: Connect your Line account and manage the conversations in Chatwoot. + - **SMS**: Connect your Twilio SMS account and reply to the SMS queries in Chatwoot. - **API Channel**: Build custom communication channels using our API channel. - - **Email (beta)**: Forward all your email queries to Chatwoot and view it in our integrated dashboard. + - **Email**: Forward all your email queries to Chatwoot and view it in our integrated dashboard. + +And more. Other features include: -- **Multi-brand inboxes**: Manage multiple brands or pages using a single dashboard. -- **Private notes**: Inter team communication is possible using private notes in a conversation. +- **CRM**: Save all your customer information right inside Chatwoot, use contact notes to log emails, phone calls, or meeting notes. +- **Custom Attributes**: Define custom attribute attributes to store information about a contact or a conversation and extend the product to match your workflow. +- **Shared multi-brand inboxes**: Manage multiple brands or pages using a shared inbox. +- **Private notes**: Use @mentions and private notes to communicate internally about a conversation. - **Canned responses (Saved replies)**: Improve the response rate by adding saved replies for frequently asked questions. -- **Conversation Labels**: Use conversation labelling to create custom workflows. +- **Conversation Labels**: Use conversation labels to create custom workflows. - **Auto assignment**: Chatwoot intelligently assigns a ticket to the agents who have access to the inbox depending on their availability and load. -- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot would send an email to the customer under the agent name so that the user can continue the conversation over the email. +- **Conversation continuity**: If the user has provided an email address through the chat widget, Chatwoot will send an email to the customer under the agent name so that the user can continue the conversation over the email. - **Multi-lingual support**: Chatwoot supports 10+ languages. - **Powerful API & Webhooks**: Extend the capability of the software using Chatwoot’s webhooks and APIs. - **Integrations**: Chatwoot natively integrates with Slack right now. Manage your conversations in Slack without logging into the dashboard. diff --git a/app/builders/contact_builder.rb b/app/builders/contact_builder.rb index ac24aca34..14f26aa82 100644 --- a/app/builders/contact_builder.rb +++ b/app/builders/contact_builder.rb @@ -33,7 +33,8 @@ class ContactBuilder phone_number: contact_attributes[:phone_number], email: contact_attributes[:email], identifier: contact_attributes[:identifier], - additional_attributes: contact_attributes[:additional_attributes] + additional_attributes: contact_attributes[:additional_attributes], + custom_attributes: contact_attributes[:custom_attributes] ) end diff --git a/app/controllers/api/v1/accounts/inbox_members_controller.rb b/app/controllers/api/v1/accounts/inbox_members_controller.rb index 591aa5637..22726a855 100644 --- a/app/controllers/api/v1/accounts/inbox_members_controller.rb +++ b/app/controllers/api/v1/accounts/inbox_members_controller.rb @@ -1,11 +1,11 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseController before_action :fetch_inbox - before_action :current_agents_ids, only: [:update] + before_action :current_agents_ids, only: [:create, :update] def create authorize @inbox, :create? ActiveRecord::Base.transaction do - params[:user_ids].map { |user_id| @inbox.add_member(user_id) } + agents_to_be_added_ids.map { |user_id| @inbox.add_member(user_id) } end fetch_updated_agents end diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 0c80d9983..1720ee692 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -124,17 +124,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def get_channel_attributes(channel_type) - case channel_type - when 'Channel::WebWidget' - Channel::WebWidget::EDITABLE_ATTRS - when 'Channel::Api' - Channel::Api::EDITABLE_ATTRS - when 'Channel::Email' - Channel::Email::EDITABLE_ATTRS - when 'Channel::Telegram' - Channel::Telegram::EDITABLE_ATTRS - when 'Channel::Line' - Channel::Line::EDITABLE_ATTRS + if channel_type.constantize.const_defined?('EDITABLE_ATTRS') + channel_type.constantize::EDITABLE_ATTRS.presence else [] end diff --git a/app/controllers/api/v1/accounts/team_members_controller.rb b/app/controllers/api/v1/accounts/team_members_controller.rb index d7e17a687..19a46b607 100644 --- a/app/controllers/api/v1/accounts/team_members_controller.rb +++ b/app/controllers/api/v1/accounts/team_members_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll def create ActiveRecord::Base.transaction do - @team_members = params[:user_ids].map { |user_id| @team.add_member(user_id) } + @team_members = members_to_be_added_ids.map { |user_id| @team.add_member(user_id) } end end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 1cbb56747..232dcff77 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -15,6 +15,7 @@ class AsyncDispatcher < BaseDispatcher EventListener.instance, HookListener.instance, InstallationWebhookListener.instance, + NotificationListener.instance, WebhookListener.instance ] end diff --git a/app/dispatchers/sync_dispatcher.rb b/app/dispatchers/sync_dispatcher.rb index 9f7adc02c..509a42727 100644 --- a/app/dispatchers/sync_dispatcher.rb +++ b/app/dispatchers/sync_dispatcher.rb @@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher end def listeners - [ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance] + [ActionCableListener.instance, AgentBotListener.instance] end end diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index 64d67701a..503c7f25b 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -1,16 +1,29 @@ module FileTypeHelper + # NOTE: video, audio, image, etc are filetypes previewable in frontend def file_type(content_type) - return :image if [ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/tiff', - 'image/bmp' - ].include?(content_type) - - return :video if content_type.include?('video/') + return :image if image_file?(content_type) + return :video if video_file?(content_type) return :audio if content_type.include?('audio/') :file end + + def image_file?(content_type) + [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/bmp', + 'image/webp' + ].include?(content_type) + end + + def video_file?(content_type) + [ + 'video/ogg', + 'video/mp4', + 'video/webm', + 'video/quicktime' + ].include?(content_type) + end end diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index 1cfd002cf..3b7eca0f7 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -12,7 +12,7 @@ padding: var(--space-normal); } -.button-wrapper .button.link.grey-btn { +.button-wrapper .button.grey-btn { margin-left: var(--space-normal); } diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 13b7dfafd..c6b7f6fd3 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -45,6 +45,9 @@ // 1. Global // --------- +// Disable contrast warnings in Foundation. +$contrast-warnings: false; + $global-font-size: 10px; $global-width: 100%; $global-lineheight: 1.5; diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index e97792b97..deba11150 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -51,6 +51,10 @@ width: 100%; } + p { + margin-bottom: 0; + } + &.multiselect__option--highlight { background: var(--white); color: var(--color-body); diff --git a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss index 55c7bc3de..4839aa9f0 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_buttons.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_buttons.scss @@ -66,6 +66,32 @@ $default-button-height: 4.0rem; } } + &.clear { + &.warning { + color: var(--y-800); + } + + &.button--only-icon:hover { + background: var(--w-50); + + &.secondary { + background: var(--s-50); + } + + &.success { + background: var(--g-50); + } + + &.alert { + background: var(--r-50); + } + + &.warning { + background: var(--y-100); + } + } + } + // Sizes &.tiny { height: var(--space-medium); diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 39cf6ad54..1be9e31e0 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -135,7 +135,7 @@ &.unread--toast { +.right { - margin-bottom: 0; + margin-bottom: var(--space-micro); } +.left { diff --git a/app/javascript/dashboard/assets/scss/widgets/_reports.scss b/app/javascript/dashboard/assets/scss/widgets/_reports.scss index eeb958ff3..bbd03ce9f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_reports.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_reports.scss @@ -21,6 +21,7 @@ border: 1px solid var(--color-border); } -.display-flex { +.reports-option__wrap { + align-items: center; display: flex; } diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 2abffb1c8..bb7858e6c 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -13,9 +13,9 @@ l