diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 62a98a872..d28ae54b7 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -21,6 +21,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def create @portal = Current.account.portals.build(portal_params) + @portal.custom_domain = parsed_custom_domain @portal.save! process_attached_logo end @@ -28,6 +29,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def update ActiveRecord::Base.transaction do @portal.update!(portal_params) if params[:portal].present? + # @portal.custom_domain = parsed_custom_domain process_attached_logo rescue StandardError => e Rails.logger.error e @@ -73,4 +75,9 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController def set_current_page @current_page = params[:page] || 1 end + + def parsed_custom_domain + domain = URI.parse(@portal.custom_domain) + domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain + end end diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 2177b715a..701bd7395 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -187,6 +187,10 @@ import { hasPressedAltAndKKey, } from 'shared/helpers/KeyboardHelpers'; import { conversationListPageURL } from '../helper/URLHelper'; +import { + isOnMentionsView, + isOnUnattendedView, +} from '../store/modules/conversations/helpers/actionHelpers'; export default { components: { @@ -652,10 +656,16 @@ export default { params: { accountId, inbox_id: inboxId, label, teamId }, name, } = this.$route; + let conversationType = ''; + if (isOnMentionsView({ route: { name } })) { + conversationType = 'mention'; + } else if (isOnUnattendedView({ route: { name } })) { + conversationType = 'unattended'; + } this.$router.push( conversationListPageURL({ accountId, - conversationType: name === 'conversation_mentions' ? 'mention' : '', + conversationType: conversationType, customViewId: this.foldersId, inboxId, label, diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 644258f50..4336b2ebf 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -87,6 +87,10 @@ import { } from 'dashboard/helper/inbox'; import SecondaryChildNavItem from './SecondaryChildNavItem'; +import { + isOnMentionsView, + isOnUnattendedView, +} from '../../../store/modules/conversations/helpers/actionHelpers'; export default { components: { SecondaryChildNavItem }, @@ -115,19 +119,31 @@ export default { this.menuItem.featureFlag ); }, - isInboxConversation() { + isAllConversations() { return ( this.$store.state.route.name === 'inbox_conversation' && this.menuItem.toStateName === 'home' ); }, + isMentions() { + return ( + isOnMentionsView({ route: this.$route }) && + this.menuItem.toStateName === 'conversation_mentions' + ); + }, + isUnattended() { + return ( + isOnUnattendedView({ route: this.$route }) && + this.menuItem.toStateName === 'conversation_unattended' + ); + }, isTeamsSettings() { return ( this.$store.state.route.name === 'settings_teams_edit' && this.menuItem.toStateName === 'settings_teams_list' ); }, - isInboxsSettings() { + isInboxSettings() { return ( this.$store.state.route.name === 'settings_inbox_show' && this.menuItem.toStateName === 'settings_inbox_list' @@ -150,14 +166,20 @@ export default { }, computedClass() { - // If active Inbox is present - // donot highlight conversations + // If active inbox is present, do not highlight conversations if (this.activeInbox) return ' '; + if ( + this.isAllConversations || + this.isMentions || + this.isUnattended || + this.isCurrentRoute + ) { + return 'is-active'; + } if (this.hasSubMenu) { if ( - this.isInboxConversation || this.isTeamsSettings || - this.isInboxsSettings || + this.isInboxSettings || this.isIntegrationsSettings || this.isApplicationsSettings ) { @@ -166,10 +188,6 @@ export default { return ' '; } - if (this.isCurrentRoute) { - return 'is-active'; - } - return ''; }, }, diff --git a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue index 893bdcf3a..9c53d4a1a 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue @@ -23,6 +23,7 @@ import 'videojs-wavesurfer/dist/videojs.wavesurfer.js'; import 'videojs-record/dist/videojs.record.js'; import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js'; import { format, addSeconds } from 'date-fns'; +import { AUDIO_FORMATS } from 'shared/constants/messages'; WaveSurfer.microphone = MicrophonePlugin; @@ -70,13 +71,26 @@ export default { record: { audio: true, video: false, - displayMilliseconds: false, - maxLength: 300, - audioEngine: 'opus-recorder', - audioWorkerURL: encoderWorker, - audioChannels: 1, - audioSampleRate: 48000, - audioBitRate: 128, + ...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && { + monitorGain: 0, + recordingGain: 1, + numberOfChannels: 1, + encoderSampleRate: 16000, + originalSampleRateOverride: 16000, + streamPages: true, + maxFramesPerPage: 1, + encoderFrameSize: 1, + encoderPath: 'opus-recorder/dist/waveWorker.min.js', + }), + ...(this.audioRecordFormat === AUDIO_FORMATS.OGG && { + displayMilliseconds: false, + maxLength: 300, + audioEngine: 'opus-recorder', + audioWorkerURL: encoderWorker, + audioChannels: 1, + audioSampleRate: 48000, + audioBitRate: 128, + }), }, }, }, @@ -86,6 +100,12 @@ export default { isRecording() { return this.player && this.player.record().isRecording(); }, + audioRecordFormat() { + if (this.isAWebWidgetInbox) { + return AUDIO_FORMATS.WEBM; + } + return AUDIO_FORMATS.OGG; + }, }, mounted() { window.Recorder = Recorder; diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index a00a3bf5a..bd47074ce 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -65,6 +65,7 @@ export default { placeholder: { type: String, default: '' }, isPrivate: { type: Boolean, default: false }, enableSuggestions: { type: Boolean, default: true }, + updateSelectionWith: { type: String, default: '' }, }, data() { return { @@ -162,6 +163,25 @@ export default { isPrivate() { this.reloadState(); }, + + updateSelectionWith(newValue, oldValue) { + if (!this.editorView) { + return null; + } + if (newValue !== oldValue) { + if (this.updateSelectionWith !== '') { + const node = this.editorView.state.schema.text( + this.updateSelectionWith + ); + const tr = this.editorView.state.tr.replaceSelectionWith(node); + this.editorView.focus(); + this.state = this.editorView.state.apply(tr); + this.emitOnChange(); + this.$emit('clear-selection'); + } + } + return null; + }, }, created() { this.state = createState(this.value, this.placeholder, this.plugins); diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index a8c26201e..bd67011e5 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -232,11 +232,18 @@ export default { return this.showFileUpload || this.isNote; }, showAudioRecorderButton() { + // Disable audio recorder for safari browser as recording is not supported + const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test( + navigator.userAgent + ); + return ( this.isFeatureEnabledonAccount( this.accountId, FEATURE_FLAGS.VOICE_RECORDER - ) && this.showAudioRecorder + ) && + this.showAudioRecorder && + !isSafari ); }, showAudioPlayStopButton() { diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index e7c825752..fdbb2fbdd 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -60,6 +60,7 @@ class="input" :is-private="isOnPrivateNote" :placeholder="messagePlaceHolder" + :update-selection-with="updateEditorSelectionWith" :min-height="4" @typing-off="onTypingOff" @typing-on="onTypingOn" @@ -67,6 +68,7 @@ @blur="onBlur" @toggle-user-mention="toggleUserMention" @toggle-canned-menu="toggleCannedMenu" + @clear-selection="clearEditorSelection" />
@@ -215,6 +217,7 @@ export default { ccEmails: '', doAutoSaveDraft: () => {}, showWhatsAppTemplatesModal: false, + updateEditorSelectionWith: '', }; }, computed: { @@ -707,8 +710,26 @@ export default { } this.$nextTick(() => this.$refs.messageInput.focus()); }, + clearEditorSelection() { + this.updateEditorSelectionWith = ''; + }, + insertEmoji(emoji, selectionStart, selectionEnd) { + const { message } = this; + const newMessage = + message.slice(0, selectionStart) + + emoji + + message.slice(selectionEnd, message.length); + this.message = newMessage; + }, emojiOnClick(emoji) { - this.message = `${this.message}${emoji} `; + if (this.showRichContentEditor) { + this.updateEditorSelectionWith = emoji; + this.onFocus(); + } + if (!this.showRichContentEditor) { + const { selectionStart, selectionEnd } = this.$refs.messageInput.$el; + this.insertEmoji(emoji, selectionStart, selectionEnd); + } }, clearMessage() { this.message = ''; diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index fcb2eab7f..68cadfb72 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -77,12 +77,16 @@ export const conversationListPageURL = ({ url = `accounts/${accountId}/label/${label}`; } else if (teamId) { url = `accounts/${accountId}/team/${teamId}`; - } else if (conversationType === 'mention') { - url = `accounts/${accountId}/mentions/conversations`; } else if (inboxId) { url = `accounts/${accountId}/inbox/${inboxId}`; } else if (customViewId) { url = `accounts/${accountId}/custom_view/${customViewId}`; + } else if (conversationType) { + const urlMap = { + mention: 'mentions/conversations', + unattended: 'unattended/conversations', + }; + url = `accounts/${accountId}/${urlMap[conversationType]}`; } return frontendURL(url); }; diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js index 4d1e1cfd7..c8511352b 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js @@ -99,6 +99,8 @@ const MUTE_ACTION = { export const isAConversationRoute = routeName => [ 'inbox_conversation', + 'conversation_through_mentions', + 'conversation_through_unattended', 'conversation_through_inbox', 'conversations_through_label', 'conversations_through_team', diff --git a/app/javascript/shared/constants/messages.js b/app/javascript/shared/constants/messages.js index e189a19a8..70235ce0e 100644 --- a/app/javascript/shared/constants/messages.js +++ b/app/javascript/shared/constants/messages.js @@ -72,3 +72,8 @@ export const CSAT_RATINGS = [ color: '#44CE4B', }, ]; + +export const AUDIO_FORMATS = { + WEBM: 'audio/webm', + OGG: 'audio/ogg', +}; diff --git a/spec/controllers/api/v1/accounts/portals_controller_spec.rb b/spec/controllers/api/v1/accounts/portals_controller_spec.rb index 8333d52e5..8f10b6b60 100644 --- a/spec/controllers/api/v1/accounts/portals_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/portals_controller_spec.rb @@ -91,7 +91,8 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do portal_params = { portal: { name: 'test_portal', - slug: 'test_kbase' + slug: 'test_kbase', + custom_domain: 'https://support.chatwoot.dev' }, logo: file } @@ -103,6 +104,7 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do json_response = JSON.parse(response.body) expect(json_response['name']).to eql('test_portal') expect(json_response['logo']['filename']).to eql('avatar.png') + expect(json_response['custom_domain']).to eql('support.chatwoot.dev') end end end