diff --git a/Gemfile b/Gemfile index 677d56c1a..591630a82 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,7 @@ gem 'redis-rack-cache' gem 'dotenv-rails' gem 'foreman' gem 'puma' -gem 'webpacker' +gem 'webpacker', '~> 5.x' ##--- gems for authentication & authorization ---## gem 'devise' diff --git a/Gemfile.lock b/Gemfile.lock index 00e93c5e1..fc5c613bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,7 +141,7 @@ GEM coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) - concurrent-ruby (1.1.6) + concurrent-ruby (1.1.7) connection_pool (2.2.3) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -242,7 +242,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.8.3) + i18n (1.8.5) concurrent-ruby (~> 1.0) ice_nine (0.11.2) inflecto (0.0.2) @@ -304,7 +304,7 @@ GEM multipart-post (2.1.1) netrc (0.11.0) nio4r (2.5.2) - nokogiri (1.10.9) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) oauth (0.5.4) orm_adapter (0.5.0) @@ -533,7 +533,7 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.1.1) + webpacker (5.2.1) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -545,7 +545,7 @@ GEM websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.0) - zeitwerk (2.3.0) + zeitwerk (2.4.0) PLATFORMS ruby @@ -625,7 +625,7 @@ DEPENDENCIES valid_email2 web-console webmock - webpacker + webpacker (~> 5.x) webpush wisper (= 2.0.0) diff --git a/README.md b/README.md index 8d9fef405..0eb841bb8 100644 --- a/README.md +++ b/README.md @@ -23,40 +23,71 @@ ___ Discord

-![ChatUI progess](https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/chatwoot-dashboard-assets.png) - -## Background - -Chatwoot is a customer support tool for instant messaging channels which can help businesses provide exceptional customer support. 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 opensource, 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. +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. -## Documentation +--- -Detailed documentation is available at [www.chatwoot.com/docs](https://www.chatwoot.com/docs). +### Features -You can find the quick setup docs [here](https://www.chatwoot.com/docs/quick-setup). +Chatwoot gives an integrated view of conversations happening in different communication channels. -## Branching model +It 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. + - **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 + - **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. + +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. +- **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. +- **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. +- **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. + +--- + +### Documentation + +Detailed documentation is available at [www.chatwoot.com/help-center](https://www.chatwoot.com/help-center). + +--- + +### Branching model We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`. If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`. -## Heroku one-click deploy +--- -Deploying chatwoot to heroku is a breeze. It's as simple as clicking this button: +### Deployment + +#### Heroku one-click deploy + +Deploying Chatwoot to Heroku is a breeze. It's as simple as clicking this button: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master) -Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables. This applies to deploying the docker image as well. +Follow this [link](https://www.chatwoot.com/docs/environment-variables) to understand setting the correct environment variables for the app to work with all the features. There might be breakages if you do not set the relevant environment variables. -## Docker +#### Other deployment options -Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`. +Please follow [deployment architecture guide](https://www.chatwoot.com/docs/deployment/architecture) to deploy with Docker or Caprover. -Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup the environment for Docker. +--- -## Contributors ✨ +### Contributors ✨ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contributors): diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 75af71a1f..ff8efd3b5 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -33,7 +33,8 @@ class ContactIdentifyAction end def update_contact - @contact.update!(params.slice(:name, :email, :identifier)) + custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes + @contact.update!(params.slice(:name, :email, :identifier).merge({ custom_attributes: custom_attributes })) ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 030c08045..dacd66e46 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -64,6 +64,6 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController end def agents - @agents ||= Current.account.users + @agents ||= Current.account.users.order_by_full_name end end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index eef50ef11..87b3898f1 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -12,14 +12,19 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def create ActiveRecord::Base.transaction do - @contact = Current.account.contacts.new(contact_create_params) + @contact = Current.account.contacts.new(contact_params) @contact.save! @contact_inbox = build_contact_inbox end end def update - @contact.update!(contact_params) + @contact.update!(contact_update_params) + rescue ActiveRecord::RecordInvalid => e + render json: { + message: e.record.errors.full_messages.join(', '), + contact: Contact.find_by(email: contact_params[:email]) + }, status: :unprocessable_entity end def search @@ -43,14 +48,21 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def contact_params - params.require(:contact).permit(:name, :email, :phone_number) + params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {}) + end + + def contact_custom_attributes + return @contact.custom_attributes.merge(contact_params[:custom_attributes]) if contact_params[:custom_attributes] + + @contact.custom_attributes + end + + def contact_update_params + # we want the merged custom attributes not the original one + contact_params.except(:custom_attributes).merge({ custom_attributes: contact_custom_attributes }) end def fetch_contact @contact = Current.account.contacts.find(params[:id]) end - - def contact_create_params - params.require(:contact).permit(:name, :email, :phone_number) - end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 74f206e00..51b6aa079 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -25,6 +25,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro head :ok end + def transcript + ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present? + head :ok + end + def toggle_status if params[:status] @conversation.status = params[:status] diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 9360b700f..d2b0dcb60 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController before_action :check_authorization def index - @inboxes = policy_scope(Current.account.inboxes.order_by_id.includes(:channel, :avatar_attachment)) + @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, :avatar_attachment)) end def create diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index 2a2b184fb..b58d26373 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -10,6 +10,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController private def permitted_params - params.permit(:website_token, :identifier, :email, :name, :avatar_url) + params.permit(:website_token, :identifier, :email, :name, :avatar_url, custom_attributes: {}) end end diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index c8d3b64a7..66b54d4b9 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -13,6 +13,16 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController head :ok end + def transcript + if permitted_params[:email].present? && conversation.present? + ConversationReplyMailer.conversation_transcript( + conversation, + permitted_params[:email] + )&.deliver_later + end + head :ok + end + def toggle_typing head :ok && return if conversation.nil? @@ -32,6 +42,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token) + params.permit(:id, :typing_status, :website_token, :email) end end diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb index e2876a58a..f10eee131 100644 --- a/app/drops/user_drop.rb +++ b/app/drops/user_drop.rb @@ -1,2 +1,5 @@ class UserDrop < BaseDrop + def available_name + @obj.try(:available_name) + end end diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 5a9173a7a..fa23fda66 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -53,6 +53,10 @@ class ConversationApi extends ApiClient { }, }); } + + sendEmailTranscript({ conversationId, email }) { + return axios.post(`${this.url}/${conversationId}/transcript`, { email }); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js index 4eceba728..ddea6b874 100644 --- a/app/javascript/dashboard/api/specs/inbox/conversation.spec.js +++ b/app/javascript/dashboard/api/specs/inbox/conversation.spec.js @@ -15,5 +15,6 @@ describe('#ConversationAPI', () => { expect(conversationAPI).toHaveProperty('toggleTyping'); expect(conversationAPI).toHaveProperty('mute'); expect(conversationAPI).toHaveProperty('meta'); + expect(conversationAPI).toHaveProperty('sendEmailTranscript'); }); }); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index 900dd4d9d..209874312 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -37,3 +37,9 @@ code { padding: $space-two; } } + +.text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 9d3650f88..50957b385 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -347,7 +347,7 @@ $helptext-color: $header-color; $helptext-font-size: $font-size-small; $helptext-font-style: italic; $input-prefix-color: $header-color; -$input-prefix-background: $light-gray; +$input-prefix-background: var(--b-100); $input-prefix-border: 1px solid $color-border; $input-prefix-padding: 1rem; $form-label-color: $header-color; diff --git a/app/javascript/dashboard/assets/scss/_helper-classes.scss b/app/javascript/dashboard/assets/scss/_helper-classes.scss index ffd9e734e..cbcc31e42 100644 --- a/app/javascript/dashboard/assets/scss/_helper-classes.scss +++ b/app/javascript/dashboard/assets/scss/_helper-classes.scss @@ -55,5 +55,17 @@ input, textarea, select { - border-radius: 4px !important; + border-radius: var(--space-smaller) !important; +} + +.input-group { + .input-group-label:first-child { + border-bottom-left-radius: var(--space-smaller); + border-top-left-radius: var(--space-smaller); + } + + .input-group-field { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; + } } diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss index 9bf19e1c2..36a87a4f9 100644 --- a/app/javascript/dashboard/assets/scss/_mixins.scss +++ b/app/javascript/dashboard/assets/scss/_mixins.scss @@ -235,3 +235,12 @@ $spinner-before-border-color: rgba(255, 255, 255, 0.7); text-overflow: ellipsis; white-space: nowrap; } + + +.justify-space-between { + justify-content: space-between; +} + +.w-100 { + width: 100%; +} diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index e1aef9307..878e5a788 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -15,10 +15,9 @@ .multiselect-box { @include flex; @include flex-align($x: justify, $y: middle); - @include margin(0 $space-small); @include border-light; border-radius: $space-smaller; - margin-right: $space-normal; + margin-right: var(--space-small); &::before { color: $medium-gray; diff --git a/app/javascript/dashboard/components/buttons/Button.vue b/app/javascript/dashboard/components/buttons/Button.vue new file mode 100644 index 000000000..ecd43f39f --- /dev/null +++ b/app/javascript/dashboard/components/buttons/Button.vue @@ -0,0 +1,43 @@ + + + diff --git a/app/javascript/dashboard/components/buttons/ResolveButton.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue similarity index 68% rename from app/javascript/dashboard/components/buttons/ResolveButton.vue rename to app/javascript/dashboard/components/buttons/ResolveAction.vue index 9b1181426..8d99d3ce3 100644 --- a/app/javascript/dashboard/components/buttons/ResolveButton.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -12,8 +12,6 @@ + + diff --git a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue index 33fbadf67..e80adf24d 100644 --- a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue +++ b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue @@ -87,11 +87,3 @@ export default { }, }; - - diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 6d46af8a1..2e0a89cb2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -4,7 +4,7 @@ v-if="currentChat.id" :inbox-id="inboxId" :is-contact-panel-open="isContactPanelOpen" - @contactPanelToggle="onToggleContactPanel" + @contact-panel-toggle="onToggleContactPanel" /> @@ -43,7 +43,7 @@ export default { }, methods: { onToggleContactPanel() { - this.$emit('contactPanelToggle'); + this.$emit('contact-panel-toggle'); }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index c5ce3d0be..65567ae26 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -1,6 +1,6 @@ diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 2689fe550..5376b1bc4 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -3,7 +3,7 @@