diff --git a/.rubocop.yml b/.rubocop.yml index 01951faea..0fca6f54b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -105,6 +105,7 @@ Rails/UniqueValidationWithoutIndex: Exclude: - 'app/models/channel/twitter_profile.rb' - 'app/models/webhook.rb' + - 'app/models/contact.rb' Rails/RenderInline: Exclude: - 'app/controllers/swagger_controller.rb' diff --git a/README.md b/README.md index bf868799f..030b13dcd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@

- Woot-logo + Woot-logo -

A simple and elegant live chat software
-
An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.
+

Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.

@@ -16,11 +15,13 @@ ___

Maintainability CircleCI Badge + Docker Pull Badge Docker Build Badge License Commits-per-month Discord + Huntr

Chat dashboard diff --git a/app/assets/stylesheets/administrate/utilities/_variables.scss b/app/assets/stylesheets/administrate/utilities/_variables.scss index 1798d918b..c46326612 100644 --- a/app/assets/stylesheets/administrate/utilities/_variables.scss +++ b/app/assets/stylesheets/administrate/utilities/_variables.scss @@ -55,7 +55,7 @@ $color-heading: #1f2d3d; $color-extra-light-blue: #f5f7f9; $primary-color: $color-woot; -$secondary-color: #35c5ff; +$secondary-color: #5d7592; $success-color: #44ce4b; $warning-color: #ffc532; $alert-color: #ff382d; diff --git a/app/builders/contact_inbox_builder.rb b/app/builders/contact_inbox_builder.rb new file mode 100644 index 000000000..73380190d --- /dev/null +++ b/app/builders/contact_inbox_builder.rb @@ -0,0 +1,41 @@ +class ContactInboxBuilder + pattr_initialize [:contact_id!, :inbox_id!, :source_id] + + def perform + @contact = Contact.find(contact_id) + @inbox = @contact.account.inboxes.find(inbox_id) + return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type + + source_id = @source_id || generate_source_id + create_contact_inbox(source_id) if source_id.present? + end + + private + + def generate_source_id + return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms' + return @contact.email if @inbox.channel_type == 'Channel::Email' + return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api' + + nil + end + + def twilio_source_id + return unless @contact.phone_number + + case @inbox.channel.medium + when 'sms' + @contact.phone_number + when 'whatsapp' + "whatsapp:#{@contact.phone_number}" + end + end + + def create_contact_inbox(source_id) + ::ContactInbox.find_or_create_by!( + contact_id: @contact.id, + inbox_id: @inbox.id, + source_id: source_id + ) + end +end diff --git a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb index 8a9199b6b..576fbaa2f 100644 --- a/app/controllers/api/v1/accounts/contacts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/contacts/conversations_controller.rb @@ -8,10 +8,10 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts:: private def inbox_ids - if current_user.administrator? + if Current.user.administrator? Current.account.inboxes.pluck(:id) - elsif current_user.agent? - current_user.assigned_inboxes.pluck(:id) + elsif Current.user.agent? + Current.user.assigned_inboxes.pluck(:id) else [] end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 37a583f05..375eb9313 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController before_action :check_authorization before_action :set_current_page, only: [:index, :active, :search] - before_action :fetch_contact, only: [:show, :update] + before_action :fetch_contact, only: [:show, :update, :contactable_inboxes] def index @contacts_count = resolved_contacts.count @@ -40,6 +40,10 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def show; end + def contactable_inboxes + @contactable_inboxes = Contacts::ContactableInboxesService.new(contact: @contact).get + end + def create ActiveRecord::Base.transaction do @contact = Current.account.contacts.new(contact_params) diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index 8cfea20ba..ffd00461b 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -4,7 +4,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: end def create - user = current_user || @resource + user = Current.user || @resource mb = Messages::MessageBuilder.new(user, @conversation, params) @message = mb.perform rescue StandardError => e diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 5dddf8036..dccb56a95 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -22,7 +22,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def create - @conversation = ::Conversation.create!(conversation_params) + ActiveRecord::Base.transaction do + @conversation = ::Conversation.create!(conversation_params) + Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present? + end end def show; end @@ -78,16 +81,29 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def contact_inbox + @contact_inbox = build_contact_inbox + @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) end + def build_contact_inbox + return if params[:contact_id].blank? || params[:inbox_id].blank? + + ContactInboxBuilder.new( + contact_id: params[:contact_id], + inbox_id: params[:inbox_id], + source_id: params[:source_id] + ).perform + end + def conversation_params + additional_attributes = params[:additional_attributes]&.permit! || {} { account_id: Current.account.id, inbox_id: @contact_inbox.inbox_id, contact_id: @contact_inbox.contact_id, contact_inbox_id: @contact_inbox.id, - additional_attributes: params[:additional_attributes] + additional_attributes: additional_attributes } end diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index 3d6f55674..91bd461aa 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -14,6 +14,7 @@ module AccessTokenAuthHelper render_unauthorized('Invalid Access Token') && return if @access_token.blank? @resource = @access_token.owner + Current.user = @resource if current_user.is_a?(User) end def super_admin? @@ -21,7 +22,7 @@ module AccessTokenAuthHelper end def validate_bot_access_token! - return if current_user.is_a?(User) + return if Current.user.is_a?(User) return if super_admin? return if agent_bot_accessible? diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 5de3e8d9b..4a1ca37f1 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -61,7 +61,9 @@ export default { }, isLoggedIn() { - return !!Cookies.getJSON('auth_data'); + const hasAuthCookie = !!Cookies.getJSON('auth_data'); + const hasUserCookie = !!Cookies.getJSON('user'); + return hasAuthCookie && hasUserCookie; }, isAdmin() { @@ -79,7 +81,9 @@ export default { }, getPubSubToken() { if (this.isLoggedIn()) { - return Cookies.getJSON('user').pubsub_token; + const user = Cookies.getJSON('user') || {}; + const { pubsub_token: pubsubToken } = user; + return pubsubToken; } return null; }, diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js index 6429c53d2..5dd0bcc57 100644 --- a/app/javascript/dashboard/api/contacts.js +++ b/app/javascript/dashboard/api/contacts.js @@ -14,6 +14,10 @@ class ContactAPI extends ApiClient { return axios.get(`${this.url}/${contactId}/conversations`); } + getContactableInboxes(contactId) { + return axios.get(`${this.url}/${contactId}/contactable_inboxes`); + } + search(search = '', page = 1) { return axios.get(`${this.url}/search?q=${search}&page=${page}`); } diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index b520156a4..9388fc3bb 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -28,8 +28,10 @@ class ConversationApi extends ApiClient { }); } - toggleStatus(conversationId) { - return axios.post(`${this.url}/${conversationId}/toggle_status`, {}); + toggleStatus({ conversationId, status }) { + return axios.post(`${this.url}/${conversationId}/toggle_status`, { + status, + }); } assignAgent({ conversationId, agentId }) { diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index e8862b5bf..8d6e63df9 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -49,7 +49,7 @@ $global-font-size: 10px; $global-width: 100%; $global-lineheight: 1.5; $foundation-palette: (primary: $color-woot, - secondary: #35c5ff, + secondary: #5d7592, success: #44ce4b, warning: #ffc532, alert: #ff382d); @@ -260,11 +260,11 @@ color 0.25s ease-out; // 12. Button Group // ---------------- -$buttongroup-margin: 1rem; -$buttongroup-spacing: 1px; +$buttongroup-margin: 0; +$buttongroup-spacing: 0; $buttongroup-child-selector: '.button'; $buttongroup-expand-max: 6; -$buttongroup-radius-on-each: true; +$buttongroup-radius-on-each: false; // 13. Callout // ----------- diff --git a/app/javascript/dashboard/assets/scss/_variables.scss b/app/javascript/dashboard/assets/scss/_variables.scss index 0e8b948c4..447e86a25 100644 --- a/app/javascript/dashboard/assets/scss/_variables.scss +++ b/app/javascript/dashboard/assets/scss/_variables.scss @@ -55,7 +55,7 @@ $color-heading: #1f2d3d; $color-extra-light-blue: #f5f7f9; $primary-color: $color-woot; -$secondary-color: #35c5ff; +$secondary-color: #5d7592; $success-color: #44ce4b; $warning-color: #ffc532; $alert-color: #ff382d; diff --git a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss index 9f70dec13..7009a077f 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_dropdown.scss @@ -1,6 +1,8 @@ .dropdown-pane { @include elegant-card; @include border-light; + box-sizing: content-box; + width: fit-content; z-index: 999; &.dropdown-pane--open { diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 36df6af14..bc024e024 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -118,6 +118,8 @@ } .multiselect__single { + @include text-ellipsis; + display: inline-block; margin-bottom: 0; padding: var(--space-slab) var(--space-one); } diff --git a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss index 69363e542..dc2a4c2d1 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conv-header.scss @@ -8,20 +8,15 @@ $resolve-button-width: 13.2rem; @include flex-align($x: justify, $y: middle); @include border-normal-bottom; - // Resolve Button - .button { - @include margin(0); - @include flex; - } - .multiselect-box { @include flex; @include flex-align($x: justify, $y: middle); - @include border-light; - border-radius: $space-smaller; + border: 1px solid var(--color-border); + border-radius: var(--space-smaller); margin-right: var(--space-small); + width: 20.2rem; - &::before { + .icon { color: $medium-gray; font-size: $font-size-default; line-height: 3.8rem; @@ -31,6 +26,7 @@ $resolve-button-width: 13.2rem; .multiselect { margin: 0; + min-width: 0; .multiselect__tags { border: 0; @@ -66,26 +62,6 @@ $resolve-button-width: 13.2rem; } } -.button.resolve--button { - @include flex-align($x: center, $y: middle); - font-size: var(--font-size-default); - width: $resolve-button-width; - - >.icon { - font-size: $font-size-default; - padding-right: $space-small; - } - - .spinner { - margin-right: $space-smaller; - padding: 0 $space-one; - - &::before { - border-top-color: $color-white; - } - } -} - .header-actions-wrap { display: flex; diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss index 297166b66..56f75ab76 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss @@ -151,6 +151,7 @@ border-top-left-radius: $space-smaller; color: $color-body; margin-right: auto; + word-break: break-word; &.is-image { border-radius: var(--border-radius-large); @@ -198,6 +199,7 @@ border-bottom-right-radius: $space-smaller; border-top-right-radius: $space-smaller; margin-left: auto; + word-break: break-word; &.is-private { background: lighten($warning-color, 32%); @@ -228,6 +230,10 @@ } } + &.center { + justify-content: center; + } + .wrap { @include margin($zero $space-normal); max-width: 69%; @@ -247,17 +253,22 @@ } .activity-wrap { - @include flex; - @include margin($space-small auto); - @include padding($space-small $space-normal); - @include flex-align($x: center, $y: null); - background: lighten($warning-color, 32%); - border: 1px solid lighten($warning-color, 22%); - border-radius: $space-smaller; - font-size: $font-size-small; + background: var(--s-50); + border: 1px solid var(--s-100); + border-radius: var(--border-radius-medium); + display: flex; + font-size: var(--font-size-small); + justify-content: center; + margin: var(--space-small) var(--space-normal); + padding: var(--space-small) var(--space-normal); - .message-text__wrap { + .is-text { display: inline-block; + text-align: start; + + @include breakpoint(xxxlarge up) { + display: flex; + } } } } diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss index 099d32287..aafff16ff 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss @@ -67,12 +67,15 @@ flex-direction: column; position: relative; + &:hover { + background: var(--color-background-light); + } + .dropdown-pane { bottom: 6rem; display: block; - left: 5rem; visibility: visible; - width: 80%; + width: fit-content; } .active { diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index a21fb15a7..392b0a7a4 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -3,7 +3,6 @@

- {{ pageTitle }}

diff --git a/app/javascript/dashboard/components/SidemenuIcon.vue b/app/javascript/dashboard/components/SidemenuIcon.vue index 1ac0b0370..f17a1c865 100644 --- a/app/javascript/dashboard/components/SidemenuIcon.vue +++ b/app/javascript/dashboard/components/SidemenuIcon.vue @@ -3,7 +3,6 @@ + diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index 6127eec43..9b3274e31 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -1,29 +1,86 @@ + diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index fff5ff899..239fbd809 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -2,7 +2,7 @@ /* eslint-env browser */ import AvatarUploader from './widgets/forms/AvatarUploader.vue'; import Bar from './widgets/chart/BarChart'; -import Button from './widgets/Button'; +import Button from './ui/WootButton'; import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; import DeleteModal from './widgets/modal/DeleteModal.vue'; diff --git a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue index 469129339..0be471a46 100644 --- a/app/javascript/dashboard/components/layout/AvailabilityStatus.vue +++ b/app/javascript/dashboard/components/layout/AvailabilityStatus.vue @@ -170,6 +170,7 @@ export default { .status-change { .dropdown-pane { top: -132px; + right: var(--space-normal); } .status-items { diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 0bb416456..509c34696 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -19,12 +19,12 @@ :menu-item="teamSection" /> @@ -128,11 +128,11 @@ export default { currentRoute() { return this.$store.state.route.name; }, - shouldShowInboxes() { + shouldShowSidebarItem() { return this.sidemenuItems.common.routes.includes(this.currentRoute); }, shouldShowTeams() { - return this.shouldShowInboxes && this.teams.length; + return this.shouldShowSidebarItem && this.teams.length; }, inboxSection() { return { diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue index a2902d071..292e4df4e 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/OptionsMenu.vue @@ -80,3 +80,8 @@ export default { }, }; + diff --git a/app/javascript/dashboard/components/ui/WootButton.vue b/app/javascript/dashboard/components/ui/WootButton.vue index e97f41f41..4df156f3a 100644 --- a/app/javascript/dashboard/components/ui/WootButton.vue +++ b/app/javascript/dashboard/components/ui/WootButton.vue @@ -12,8 +12,8 @@ @click="handleClick" > - - + + + diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue index 84d6c7277..2d5ded515 100644 --- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue +++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue @@ -11,9 +11,7 @@ class="image-thumb" :src="attachment.thumb" /> - - 📄 - + 📄
@@ -37,8 +35,7 @@
diff --git a/app/javascript/dashboard/helper/files.js b/app/javascript/dashboard/helper/files.js deleted file mode 100644 index 4f7559304..000000000 --- a/app/javascript/dashboard/helper/files.js +++ /dev/null @@ -1,11 +0,0 @@ -export const formatBytes = (bytes, decimals = 2) => { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -}; diff --git a/app/javascript/dashboard/helper/specs/files.spec.js b/app/javascript/dashboard/helper/specs/files.spec.js deleted file mode 100644 index 3b2c86bcd..000000000 --- a/app/javascript/dashboard/helper/specs/files.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import { formatBytes } from '../files'; - -describe('#File Helpers', () => { - describe('formatBytes', () => { - it('should return zero bytes if 0 is passed', () => { - expect(formatBytes(0)).toBe('0 Bytes'); - }); - it('should return in bytes if 1000 is passed', () => { - expect(formatBytes(1000)).toBe('1000 Bytes'); - }); - it('should return in KB if 100000 is passed', () => { - expect(formatBytes(10000)).toBe('9.77 KB'); - }); - it('should return in MB if 10000000 is passed', () => { - expect(formatBytes(10000000)).toBe('9.54 MB'); - }); - }); -}); diff --git a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json index b08032106..e9b0fc91c 100644 --- a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json @@ -88,7 +88,7 @@ "SUBMIT_BUTTON": "Δημιουργία Κιβωτίου" }, "TWILIO": { - "TITLE": "SMS κανάλι από το Twillio", + "TITLE": "SMS κανάλι από το Twilio", "DESC": "Ενσωματώστε το Twilio και αρχίστε να υποστηρίζετε τους πελάτες σας μέσω SMS.", "ACCOUNT_SID": { "LABEL": "SID Λογαριασμού", @@ -116,7 +116,7 @@ }, "API_CALLBACK": { "TITLE": "URL επανάκλησης", - "SUBTITLE": "Πρέπει να ρυθμίσετε το callback URL στο Twillio με το URL που αναφέρεται εδώ." + "SUBTITLE": "Πρέπει να ρυθμίσετε το callback URL στο Twilio με το URL που αναφέρεται εδώ." }, "SUBMIT_BUTTON": "Δημιουργία Καναλιού Twillio", "API": { diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index d6cc103ca..b8d936d22 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -12,6 +12,7 @@ "INITIATED_FROM": "Initiated from", "INITIATED_AT": "Initiated at", "IP_ADDRESS": "IP Address", + "NEW_MESSAGE": "New message", "CONVERSATIONS": { "NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.", "TITLE": "Previous Conversations" @@ -71,7 +72,9 @@ }, "PHONE_NUMBER": { "PLACEHOLDER": "Enter the phone number of the contact", - "LABEL": "Phone Number" + "LABEL": "Phone Number", + "HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]", + "ERROR": "Phone number should be either empty or of E.164 format" }, "LOCATION": { "PLACEHOLDER": "Enter the location of the contact", @@ -104,6 +107,30 @@ "CONTACT_ALREADY_EXIST": "This email address is in use for another contact.", "ERROR_MESSAGE": "There was an error, please try again" }, + "NEW_CONVERSATION": { + "BUTTON_LABEL": "Start conversation", + "TITLE": "New conversation", + "DESC": "Start a new conversation by sending a new message.", + "NO_INBOX": "Couldn't find an inbox to initiate a new conversation with this contact.", + "FORM": { + "TO": { + "LABEL": "To" + }, + "INBOX": { + "LABEL": "Inbox", + "ERROR": "Select an inbox" + }, + "MESSAGE": { + "LABEL": "Message", + "PLACEHOLDER": "Write your message here", + "ERROR": "Message can't be empty" + }, + "SUBMIT": "Send message", + "CANCEL": "Cancel", + "SUCCESS_MESSAGE": "Message sent!", + "ERROR_MESSAGE": "Couldn't send! try again" + } + }, "CONTACTS_PAGE": { "HEADER": "Contacts", "SEARCH_BUTTON": "Search", diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 91247105d..b13404e91 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -20,6 +20,8 @@ "LOADING_CONVERSATIONS": "Loading Conversations", "CANNOT_REPLY": "You cannot reply due to", "24_HOURS_WINDOW": "24 hour message window restriction", + "TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to", + "TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction", "LAST_INCOMING_TWEET": "You are replying to the last incoming tweet", "REPLYING_TO": "You are replying to:", "REMOVE_SELECTION": "Remove Selection", @@ -29,10 +31,14 @@ "HEADER": { "RESOLVE_ACTION": "Resolve", "REOPEN_ACTION": "Reopen", + "OPEN_ACTION": "Open", "OPEN": "More", "CLOSE": "Close", "DETAILS": "details" }, + "RESOLVE_DROPDOWN": { + "OPEN_BOT": "Open with bot" + }, "FOOTER": { "MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.", "PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents" @@ -52,6 +58,7 @@ "CHANGE_STATUS": "Conversation status changed", "CHANGE_AGENT": "Conversation Assignee changed", "CHANGE_TEAM": "Conversation team changed", + "FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit", "SENT_BY": "Sent by:", "ASSIGNMENT": { "SELECT_AGENT": "Select Agent", @@ -108,5 +115,4 @@ "PLACEHOLDER": "None" } } - } diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index e63a40656..a1184f5db 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -5,7 +5,8 @@ "LIST": { "404": "There are no inboxes attached to this account." }, - "CREATE_FLOW": [{ + "CREATE_FLOW": [ + { "title": "Choose Channel", "route": "settings_inbox_new", "body": "Choose the provider you want to integrate with Chatwoot." @@ -193,6 +194,7 @@ "TITLE": "Your Inbox is ready!", "MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting ", "BUTTON_TEXT": "Take me there", + "MORE_SETTINGS": "More settings", "WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox." }, "REAUTH": "Reauthorize", diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index fd915fb0c..314bbbbda 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -3,6 +3,8 @@ "LINK": "Profile Settings", "TITLE": "Profile Settings", "BTN_TEXT": "Update Profile", + "UPDATE_SUCCESS": "Your profile has been updated successfully", + "PASSWORD_UPDATE_SUCCESS": "Your password has been changed successfully", "AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed", "FORM": { "AVATAR": "Profile Image", @@ -16,7 +18,8 @@ }, "PASSWORD_SECTION": { "TITLE": "Password", - "NOTE": "Updating your password would reset your logins in multiple devices." + "NOTE": "Updating your password would reset your logins in multiple devices.", + "BTN_TEXT": "Change password" }, "ACCESS_TOKEN": { "TITLE": "Access Token", @@ -64,11 +67,7 @@ }, "AVAILABILITY": { "LABEL": "Availability", - "STATUSES_LIST": [ - "Online", - "Busy", - "Offline" - ] + "STATUSES_LIST": ["Online", "Busy", "Offline"] }, "EMAIL": { "LABEL": "Your email address", @@ -147,6 +146,5 @@ }, "SUBMIT": "Submit" } - } } diff --git a/app/javascript/dashboard/i18n/locale/id/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/id/inboxMgmt.json index c3137e769..fdae3d169 100644 --- a/app/javascript/dashboard/i18n/locale/id/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/id/inboxMgmt.json @@ -118,7 +118,7 @@ "TITLE": "URL Callback", "SUBTITLE": "Anda harus mengkonfigurasi pesan URL callback di Twilio dengan URL yang disebutkan di sini." }, - "SUBMIT_BUTTON": "Buat Channel Twillio", + "SUBMIT_BUTTON": "Buat Channel Twilio", "API": { "ERROR_MESSAGE": "Kami tidak dapat mengautentikasi kredensial Twilio, harap coba lagi" } diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue index 45cc635ad..32082532e 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue @@ -3,7 +3,7 @@ - + {}, @@ -135,8 +139,9 @@ export default { computed: { ...mapGetters({ currentChat: 'getSelectedChat', - agents: 'agents/getVerifiedAgents', teams: 'teams/getTeams', + getAgents: 'inboxMembers/getMembersByInbox', + uiFlags: 'inboxMembers/getUIFlags', }), currentConversationMetaData() { return this.$store.getters[ @@ -182,7 +187,7 @@ export default { return ''; } const countryFlag = countryCode ? flag(countryCode) : '🌎'; - return `${countryFlag} ${cityAndCountry}`; + return `${cityAndCountry} ${countryFlag}`; }, platformName() { const { @@ -201,7 +206,7 @@ export default { return this.$store.getters['contacts/getContact'](this.contactId); }, agentsList() { - return [{ id: 0, name: 'None' }, ...this.agents]; + return [{ id: 0, name: 'None' }, ...this.getAgents(this.inboxId)]; }, teamsList() { return [{ id: 0, name: 'None' }, ...this.teams]; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue index a1beaa13e..0fe9a1a4d 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue @@ -35,12 +35,26 @@
- +
+ +
+ {{ $t('CONTACT_FORM.FORM.PHONE_NUMBER.HELP') }} +
+
{{ $t('EDIT_CONTACT.BUTTON_LABEL') }} +
+ + {{ $t('CONTACT_PANEL.NEW_MESSAGE') }} + + + {{ $t('EDIT_CONTACT.BUTTON_LABEL') }} + +
+ @@ -70,6 +93,7 @@ import ContactInfoRow from './ContactInfoRow'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import SocialIcons from './SocialIcons'; import EditContact from './EditContact'; +import NewConversation from './NewConversation'; export default { components: { @@ -77,6 +101,7 @@ export default { EditContact, Thumbnail, SocialIcons, + NewConversation, }, props: { contact: { @@ -87,10 +112,15 @@ export default { type: String, default: '', }, + showNewMessage: { + type: Boolean, + default: false, + }, }, data() { return { showEditModal: false, + showConversationModal: false, }; }, computed: { @@ -110,6 +140,9 @@ export default { toggleEditModal() { this.showEditModal = !this.showEditModal; }, + toggleConversationModal() { + this.showConversationModal = !this.showConversationModal; + }, }, }; @@ -119,7 +152,7 @@ export default { @import '~dashboard/assets/scss/mixins'; .contact--profile { align-items: flex-start; - padding: var(--space-normal) var(--space-normal) var(--space-large); + padding: var(--space-normal) var(--space-normal); .user-thumbnail-box { margin-right: $space-normal; @@ -163,10 +196,21 @@ export default { font-size: $font-weight-normal; } } +.contact-actions { + margin: var(--space-small) 0; +} +.button.edit-contact { + margin-left: var(--space-two); + padding-left: var(--space-micro); +} -.edit-contact { - padding: 0 var(--space-slab); - margin-left: var(--space-slab); - margin-top: var(--space-smaller); +.button.new-message { + margin-right: var(--space-small); +} + +.contact-actions { + display: flex; + align-items: center; + width: 100%; } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue new file mode 100644 index 000000000..75e333a73 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ConversationForm.vue @@ -0,0 +1,210 @@ +