Merge branch 'develop' of https://github.com/chatwoot/chatwoot into chore/chat-list-design
This commit is contained in:
commit
eb9d9ace96
83 changed files with 1807 additions and 314 deletions
|
@ -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'
|
||||
|
|
|
@ -28,9 +28,7 @@ module.exports = {
|
|||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
"vue/no-v-html": 'off',
|
||||
'vue/singleline-html-element-content-newline': 'warn',
|
||||
'vue/require-default-prop': 'warn',
|
||||
'vue/require-prop-types': 'warn',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'import/extensions': ['off']
|
||||
|
||||
},
|
||||
|
|
4
Gemfile
4
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'
|
||||
|
|
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -124,6 +124,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def get_channel_attributes(channel_type)
|
||||
channel_type.constantize::EDITABLE_ATTRS.presence || []
|
||||
if channel_type.constantize.const_defined?('EDITABLE_ATTRS')
|
||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ class AsyncDispatcher < BaseDispatcher
|
|||
EventListener.instance,
|
||||
HookListener.instance,
|
||||
InstallationWebhookListener.instance,
|
||||
NotificationListener.instance,
|
||||
WebhookListener.instance
|
||||
]
|
||||
end
|
||||
|
|
|
@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
|
|||
end
|
||||
|
||||
def listeners
|
||||
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
|
||||
[ActionCableListener.instance, AgentBotListener.instance]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: String,
|
||||
message: { type: String, default: '' },
|
||||
showButton: Boolean,
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
|
|
|
@ -52,12 +52,16 @@
|
|||
<woot-dropdown-item v-if="!isPending">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="book-clock"
|
||||
@click="() => toggleStatus(STATUS_TYPE.PENDING)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.MARK_PENDING') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
|
||||
<woot-dropdown-divider v-if="isOpen" />
|
||||
<woot-dropdown-sub-menu
|
||||
v-if="isOpen"
|
||||
:title="this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.TITLE')"
|
||||
|
@ -65,6 +69,9 @@
|
|||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="send-clock"
|
||||
@click="() => toggleStatus(STATUS_TYPE.SNOOZED, null)"
|
||||
>
|
||||
{{ this.$t('CONVERSATION.RESOLVE_DROPDOWN.SNOOZE.NEXT_REPLY') }}
|
||||
|
@ -73,6 +80,9 @@
|
|||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="dual-screen-clock"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.tomorrow)
|
||||
"
|
||||
|
@ -83,6 +93,9 @@
|
|||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="calendar-clock"
|
||||
@click="
|
||||
() => toggleStatus(STATUS_TYPE.SNOOZED, snoozeTimes.nextWeek)
|
||||
"
|
||||
|
@ -110,6 +123,8 @@ import {
|
|||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
|
||||
|
||||
import wootConstants from '../../constants';
|
||||
import {
|
||||
getUnixTime,
|
||||
|
@ -129,6 +144,7 @@ export default {
|
|||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
WootDropdownSubMenu,
|
||||
WootDropdownDivider,
|
||||
},
|
||||
mixins: [clickaway, alertMixin, eventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
|
@ -269,5 +285,6 @@ export default {
|
|||
margin-top: var(--space-micro);
|
||||
right: 0;
|
||||
max-width: 20rem;
|
||||
min-width: 15.6rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
:class="{ 'is-active': isActive }"
|
||||
@click="e => handleProfileSettingClick(e, navigate)"
|
||||
>
|
||||
<fluent-icon icon="person" class="icon icon--font" />
|
||||
<fluent-icon icon="person" size="14" class="icon icon--font" />
|
||||
<span class="button__content">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</span>
|
||||
|
|
|
@ -18,12 +18,11 @@
|
|||
export default {
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
isFullwidth: Boolean,
|
||||
type: String,
|
||||
size: String,
|
||||
type: { type: String, default: '' },
|
||||
size: { type: String, default: '' },
|
||||
checked: Boolean,
|
||||
name: String,
|
||||
id: String,
|
||||
name: { type: String, default: '' },
|
||||
id: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import TWEEN from 'tween.js';
|
||||
|
||||
export default {
|
||||
name: 'WootTabsItem',
|
||||
props: {
|
||||
|
@ -57,19 +55,13 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
animatedNumber: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
active() {
|
||||
return this.index === this.$parent.index;
|
||||
},
|
||||
|
||||
getItemCount() {
|
||||
return this.animatedNumber || this.count;
|
||||
return this.count;
|
||||
},
|
||||
colorScheme() {
|
||||
if (this.variant === 'smooth') return 'secondary';
|
||||
|
@ -84,27 +76,6 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
count(newValue, oldValue) {
|
||||
let animationFrame;
|
||||
const animate = time => {
|
||||
TWEEN.update(time);
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
};
|
||||
const tweeningNumber = { value: oldValue };
|
||||
new TWEEN.Tween(tweeningNumber)
|
||||
.easing(TWEEN.Easing.Quadratic.Out)
|
||||
.to({ value: newValue }, 500)
|
||||
.onUpdate(() => {
|
||||
this.animatedNumber = tweeningNumber.value.toFixed(0);
|
||||
})
|
||||
.onComplete(() => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
})
|
||||
.start();
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTabClick(event) {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -91,7 +91,7 @@ export default {
|
|||
this.isRounded ? '' : 'not-rounded',
|
||||
];
|
||||
},
|
||||
withTextIconSize() {
|
||||
iconSize() {
|
||||
switch (this.size) {
|
||||
case 'tiny':
|
||||
return 12;
|
||||
|
@ -106,26 +106,6 @@ export default {
|
|||
return 16;
|
||||
}
|
||||
},
|
||||
withoutTextIconSize() {
|
||||
switch (this.size) {
|
||||
case 'tiny':
|
||||
return 14;
|
||||
case 'small':
|
||||
return 16;
|
||||
case 'medium':
|
||||
return 18;
|
||||
case 'large':
|
||||
return 20;
|
||||
|
||||
default:
|
||||
return 18;
|
||||
}
|
||||
},
|
||||
iconSize() {
|
||||
return this.hasOnlyIcon
|
||||
? this.withoutTextIconSize
|
||||
: this.withTextIconSize;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick(evt) {
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
message: String,
|
||||
buttonText: String,
|
||||
title: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
buttonText: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="row loading-state">
|
||||
<h6 class="message">{{ message }}<span class="spinner"></span></h6>
|
||||
<h6 class="message">{{ message }}<span class="spinner" /></h6>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: String,
|
||||
message: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
heading: String,
|
||||
point: [Number, String],
|
||||
index: Number,
|
||||
desc: String,
|
||||
heading: { type: String, default: '' },
|
||||
point: { type: [Number, String], default: '' },
|
||||
index: { type: Number, default: null },
|
||||
desc: { type: String, default: '' },
|
||||
selected: Boolean,
|
||||
onClick: Function,
|
||||
onClick: { type: Function, default: () => {} },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<file-upload
|
||||
ref="upload"
|
||||
:size="4096 * 4096"
|
||||
accept="image/png, image/jpeg, image/gif, image/bmp, image/tiff, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
:accept="allowedFileTypes"
|
||||
:drop="true"
|
||||
:drop-directory="false"
|
||||
@input-file="onFileUpload"
|
||||
|
@ -84,6 +84,7 @@ import {
|
|||
hasPressedAltAndAKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
|
@ -161,6 +162,9 @@ export default {
|
|||
showAttachButton() {
|
||||
return this.showFileUpload || this.isNote;
|
||||
},
|
||||
allowedFileTypes() {
|
||||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
:email-attributes="contentAttributes.email"
|
||||
:cc="emailHeadAttributes.cc"
|
||||
|
@ -24,9 +24,10 @@
|
|||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image'"
|
||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||
<source :src="attachment.data_url" />
|
||||
|
@ -72,8 +73,18 @@
|
|||
{{ sender.name }}
|
||||
</div>
|
||||
</a>
|
||||
<div v-if="isFailed" class="message-failed--alert">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
|
||||
size="tiny"
|
||||
color-scheme="alert"
|
||||
variant="clear"
|
||||
icon="arrow-clockwise"
|
||||
@click="retrySendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-wrap">
|
||||
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
|
||||
<context-menu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
:is-open="showContextMenu"
|
||||
|
@ -133,6 +144,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showContextMenu: false,
|
||||
hasImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -246,10 +258,13 @@ export default {
|
|||
hasText() {
|
||||
return !!this.data.content;
|
||||
},
|
||||
sentByMessage() {
|
||||
messageToolTip() {
|
||||
if (this.isMessageDeleted) {
|
||||
return false;
|
||||
}
|
||||
if (this.isFailed) {
|
||||
return this.$t(`CONVERSATION.SEND_FAILED`);
|
||||
}
|
||||
const { sender } = this;
|
||||
return this.data.message_type === 1 && !isEmptyObject(sender)
|
||||
? {
|
||||
|
@ -263,6 +278,7 @@ export default {
|
|||
wrap: this.isBubble,
|
||||
'activity-wrap': !this.isBubble,
|
||||
'is-pending': this.isPending,
|
||||
'is-failed': this.isFailed,
|
||||
};
|
||||
},
|
||||
bubbleClass() {
|
||||
|
@ -273,26 +289,45 @@ export default {
|
|||
'is-video': this.hasMediaAttachment('video'),
|
||||
'is-text': this.hasText,
|
||||
'is-from-bot': this.isSentByBot,
|
||||
'is-failed': this.isFailed,
|
||||
};
|
||||
},
|
||||
isPending() {
|
||||
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
||||
},
|
||||
isFailed() {
|
||||
return this.data.status === MESSAGE_STATUS.FAILED;
|
||||
},
|
||||
isSentByBot() {
|
||||
if (this.isPending) return false;
|
||||
if (this.isPending || this.isFailed) return false;
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
contextMenuPosition() {
|
||||
const { message_type: messageType } = this.data;
|
||||
return messageType ? 'right' : 'left';
|
||||
},
|
||||
shouldShowContextMenu() {
|
||||
return !(this.isFailed || this.isPending);
|
||||
},
|
||||
errorMessage() {
|
||||
const { meta } = this.data;
|
||||
return meta ? meta.error : '';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
methods: {
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type;
|
||||
return fileType === type && !this.hasImageError;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
@ -317,6 +352,12 @@ export default {
|
|||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||
this.showContextMenu = false;
|
||||
},
|
||||
async retrySendMessage() {
|
||||
await this.$store.dispatch('sendMessageWithData', this.data);
|
||||
},
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -383,6 +424,14 @@ export default {
|
|||
color: var(--v-50);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-failed {
|
||||
background: var(--r-200);
|
||||
|
||||
.message-text--metadata .time {
|
||||
color: var(--r-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
|
@ -413,6 +462,13 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.message-failed--alert {
|
||||
color: var(--r-900);
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
margin-top: var(--space-smaller) var(--space-smaller) 0 0;
|
||||
}
|
||||
|
||||
.button--delete-message {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
@ -438,6 +494,17 @@ li.right .context-menu-wrap {
|
|||
li.right {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-end;
|
||||
|
||||
.wrap.is-pending {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wrap.is-failed {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.has-context-menu {
|
||||
|
|
|
@ -369,7 +369,10 @@ export default {
|
|||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
this.clearMessage();
|
||||
try {
|
||||
await this.$store.dispatch('sendMessage', messagePayload);
|
||||
await this.$store.dispatch(
|
||||
'createPendingMessageAndSend',
|
||||
messagePayload
|
||||
);
|
||||
this.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="image message-text__wrap">
|
||||
<img :src="url" @click="onClick" />
|
||||
<img :src="url" @click="onClick" @error="onImgError()" />
|
||||
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
|
||||
<img :src="url" class="modal-image" />
|
||||
</woot-modal>
|
||||
|
@ -9,6 +9,7 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
|
@ -27,6 +28,9 @@ export default {
|
|||
onClick() {
|
||||
this.show = true;
|
||||
},
|
||||
onImgError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -21,12 +21,12 @@ export default {
|
|||
},
|
||||
props: {
|
||||
show: Boolean,
|
||||
onClose: Function,
|
||||
onConfirm: Function,
|
||||
title: String,
|
||||
message: String,
|
||||
confirmText: String,
|
||||
rejectText: String,
|
||||
onClose: { type: Function, default: () => {} },
|
||||
onConfirm: { type: Function, default: () => {} },
|
||||
title: { type: String, default: '' },
|
||||
message: { type: String, default: '' },
|
||||
confirmText: { type: String, default: '' },
|
||||
rejectText: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -90,6 +90,8 @@
|
|||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"SEND_FAILED": "Couldn't send message! Try again",
|
||||
"TRY_AGAIN": "retry",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
"REMOVE": "Remove",
|
||||
|
|
|
@ -57,9 +57,9 @@ export default {
|
|||
WootSubmitButton,
|
||||
},
|
||||
props: {
|
||||
resetPasswordToken: String,
|
||||
redirectUrl: String,
|
||||
config: String,
|
||||
resetPasswordToken: { type: String, default: '' },
|
||||
redirectUrl: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: String,
|
||||
headerContent: String,
|
||||
headerTitle: { type: String, default: '' },
|
||||
headerContent: { type: String, default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -25,9 +25,9 @@ export default {
|
|||
SettingsHeader,
|
||||
},
|
||||
props: {
|
||||
headerTitle: String,
|
||||
headerButtonText: String,
|
||||
icon: String,
|
||||
headerTitle: { type: String, default: '' },
|
||||
headerButtonText: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
keepAlive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
|
|
@ -61,10 +61,10 @@ export default {
|
|||
Modal,
|
||||
},
|
||||
props: {
|
||||
id: Number,
|
||||
edcontent: String,
|
||||
edshortCode: String,
|
||||
onClose: Function,
|
||||
id: { type: Number, default: null },
|
||||
edcontent: { type: String, default: '' },
|
||||
edshortCode: { type: String, default: '' },
|
||||
onClose: { type: Function, default: () => {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -32,7 +32,11 @@
|
|||
:label="inboxNameLabel"
|
||||
:placeholder="inboxNamePlaceHolder"
|
||||
/>
|
||||
<label for="toggle-business-hours" class="toggle-input-wrap" v-if="isATwitterInbox">
|
||||
<label
|
||||
v-if="isATwitterInbox"
|
||||
for="toggle-business-hours"
|
||||
class="toggle-input-wrap"
|
||||
>
|
||||
<input
|
||||
v-model="tweetsEnabled"
|
||||
type="checkbox"
|
||||
|
|
|
@ -47,8 +47,8 @@ export default {
|
|||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
integrationId: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationLogo: {
|
||||
type: String,
|
||||
|
|
|
@ -68,14 +68,17 @@ import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
|||
|
||||
export default {
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
props: [
|
||||
'integrationId',
|
||||
'integrationLogo',
|
||||
'integrationName',
|
||||
'integrationDescription',
|
||||
'integrationEnabled',
|
||||
'integrationAction',
|
||||
],
|
||||
props: {
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
integrationLogo: { type: String, default: '' },
|
||||
integrationName: { type: String, default: '' },
|
||||
integrationDescription: { type: String, default: '' },
|
||||
integrationEnabled: { type: Boolean, default: false },
|
||||
integrationAction: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
|
|
|
@ -33,7 +33,14 @@ export default {
|
|||
IntegrationHelpText,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: ['integrationId', 'code'],
|
||||
|
||||
props: {
|
||||
integrationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
code: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
integrationLoaded: false,
|
||||
|
|
|
@ -158,17 +158,33 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
sendMessage: async ({ commit }, data) => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
createPendingMessageAndSend: async ({ dispatch }, data) => {
|
||||
const pendingMessage = createPendingMessage(data);
|
||||
dispatch('sendMessageWithData', pendingMessage);
|
||||
},
|
||||
|
||||
sendMessageWithData: async ({ commit }, pendingMessage) => {
|
||||
try {
|
||||
const pendingMessage = createPendingMessage(data);
|
||||
commit(types.ADD_MESSAGE, pendingMessage);
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...pendingMessage,
|
||||
status: MESSAGE_STATUS.PROGRESS,
|
||||
});
|
||||
const response = await MessageApi.create(pendingMessage);
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...response.data,
|
||||
status: MESSAGE_STATUS.SENT,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error.response
|
||||
? error.response.data.error
|
||||
: undefined;
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...pendingMessage,
|
||||
meta: {
|
||||
error: errorMessage,
|
||||
},
|
||||
status: MESSAGE_STATUS.FAILED,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path :d="icons[`${icon}-${type}`]" fill="currentColor" />
|
||||
</svg>
|
||||
<base-icon :size="size" :icon="icon" :type="type" :icons="icons" />
|
||||
</template>
|
||||
<script>
|
||||
import BaseIcon from './Icon';
|
||||
import icons from './dashboard-icons.json';
|
||||
|
||||
export default {
|
||||
name: 'FluentIcon',
|
||||
components: {
|
||||
BaseIcon,
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
|
|
49
app/javascript/shared/components/FluentIcon/Icon.vue
Normal file
49
app/javascript/shared/components/FluentIcon/Icon.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
v-for="source in pathSource"
|
||||
:key="source"
|
||||
:d="source"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icons: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '20',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'outline',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
pathSource() {
|
||||
// To support icons with multiple paths
|
||||
const path = this.icons[`${this.icon}-${this.type}`];
|
||||
if (path.constructor === Array) {
|
||||
return path;
|
||||
}
|
||||
return [path];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,27 +1,23 @@
|
|||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path :d="icons[`${icon}-${type}`]" fill="currentColor" />
|
||||
</svg>
|
||||
<base-icon :size="size" :icon="icon" :type="type" :icons="icons" />
|
||||
</template>
|
||||
<script>
|
||||
import BaseIcon from './Icon';
|
||||
import icons from './icons.json';
|
||||
|
||||
export default {
|
||||
name: 'FluentIcon',
|
||||
components: {
|
||||
BaseIcon,
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '20px',
|
||||
type: [String, Number],
|
||||
default: '20',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"alert-outline": "M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z",
|
||||
"arrow-chevron-left-outline": "M15 17.898c0 1.074-1.265 1.648-2.073.941l-6.31-5.522a1.75 1.75 0 0 1 0-2.634l6.31-5.522c.808-.707 2.073-.133 2.073.941v11.796Z",
|
||||
"arrow-chevron-right-outline": "M9 17.898c0 1.074 1.265 1.648 2.073.941l6.31-5.522a1.75 1.75 0 0 0 0-2.634l-6.31-5.522C10.265 4.454 9 5.028 9 6.102v11.796Z",
|
||||
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
|
||||
"arrow-download-outline": "M18.25 20.5a.75.75 0 1 1 0 1.5l-13 .004a.75.75 0 1 1 0-1.5l13-.004ZM11.648 2.012l.102-.007a.75.75 0 0 1 .743.648l.007.102-.001 13.685 3.722-3.72a.75.75 0 0 1 .976-.073l.085.073a.75.75 0 0 1 .072.976l-.073.084-4.997 4.997a.75.75 0 0 1-.976.073l-.085-.073-5.003-4.996a.75.75 0 0 1 .976-1.134l.084.072 3.719 3.714L11 2.755a.75.75 0 0 1 .648-.743l.102-.007-.102.007Z",
|
||||
"arrow-redo-outline": "M19.25 2a.75.75 0 0 0-.743.648l-.007.102v5.69l-4.574-4.56a6.41 6.41 0 0 0-8.878-.179l-.186.18a6.41 6.41 0 0 0 0 9.063l8.845 8.84a.75.75 0 0 0 1.06-1.062l-8.845-8.838a4.91 4.91 0 0 1 6.766-7.112l.178.17L17.438 9.5H11.75a.75.75 0 0 0-.743.648L11 10.25c0 .38.282.694.648.743l.102.007h7.5a.75.75 0 0 0 .743-.648L20 10.25v-7.5a.75.75 0 0 0-.75-.75Z",
|
||||
"arrow-reply-outline": "M9.277 16.221a.75.75 0 0 1-1.061 1.06l-4.997-5.003a.75.75 0 0 1 0-1.06L8.217 6.22a.75.75 0 0 1 1.061 1.06L5.557 11h7.842c1.595 0 2.81.242 3.889.764l.246.126a6.203 6.203 0 0 1 2.576 2.576c.61 1.14.89 2.418.89 4.135a.75.75 0 0 1-1.5 0c0-1.484-.228-2.52-.713-3.428a4.702 4.702 0 0 0-1.96-1.96c-.838-.448-1.786-.676-3.094-.709L13.4 12.5H5.562l3.715 3.721Z",
|
||||
|
@ -14,7 +15,9 @@
|
|||
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
|
||||
"autocorrect-outline": "M13.461 4.934c.293.184.548.42.752.698l.117.171 2.945 4.696H21.5a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.75.75l-3.284-.001.006.009-.009-.01a4.75 4.75 0 1 1-3.463-1.5h.756L13.059 6.6a1.25 1.25 0 0 0-2.04-.112l-.078.112-7.556 12.048a.75.75 0 0 1-1.322-.699l.052-.098L9.67 5.803a2.75 2.75 0 0 1 3.791-.869ZM14.751 12a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Z",
|
||||
"book-contacts-outline": "M15.5 12.25a.75.75 0 0 0-.75-.75h-5a.75.75 0 0 0-.75.75v.5c0 1 1.383 1.75 3.25 1.75s3.25-.75 3.25-1.75v-.5ZM14 8.745C14 7.78 13.217 7 12.25 7s-1.75.779-1.75 1.745a1.75 1.75 0 1 0 3.5 0ZM4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15Zm1.5 0V18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1Z",
|
||||
"book-clock-outline": ["M13 9.125v1.625h.75a.625.625 0 1 1 0 1.25H12.5a.615.615 0 0 1-.063-.003.625.625 0 0 1-.688-.622v-2.25a.625.625 0 1 1 1.251 0Z", "M12.375 6.005a4.75 4.75 0 1 0 0 9.5 4.75 4.75 0 0 0 0-9.5Zm-3.5 4.75a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Z", "M6.5 2A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h13.25a.75.75 0 0 0 0-1.5H6.5a1 1 0 0 1-1-1h14.25a.75.75 0 0 0 .75-.75V4.5A2.5 2.5 0 0 0 18 2H6.5ZM19 18H5.5V4.5a1 1 0 0 1 1-1H18a1 1 0 0 1 1 1V18Z"],
|
||||
"building-bank-outline": "M13.032 2.325a1.75 1.75 0 0 0-2.064 0L3.547 7.74c-.978.713-.473 2.26.736 2.26H4.5v5.8A2.75 2.75 0 0 0 3 18.25v1.5c0 .413.336.75.75.75h16.5a.75.75 0 0 0 .75-.75v-1.5a2.75 2.75 0 0 0-1.5-2.45V10h.217c1.21 0 1.713-1.547.736-2.26l-7.421-5.416Zm-1.18 1.211a.25.25 0 0 1 .295 0L18.95 8.5H5.05l6.803-4.964ZM18 10v5.5h-2V10h2Zm-3.5 0v5.5h-1.75V10h1.75Zm-3.25 0v5.5H9.5V10h1.75Zm-5.5 7h12.5c.69 0 1.25.56 1.25 1.25V19h-15v-.75c0-.69.56-1.25 1.25-1.25ZM6 15.5V10h2v5.5H6Z",
|
||||
"calendar-clock-outline": ["M21 6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h5.772a6.471 6.471 0 0 1-.709-1.5H6.25a1.75 1.75 0 0 1-1.75-1.75V8.5h15v2.813a6.471 6.471 0 0 1 1.5.709V6.25ZM6.25 4.5h11.5c.966 0 1.75.784 1.75 1.75V7h-15v-.75c0-.966.784-1.75 1.75-1.75Z", "M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"],
|
||||
"call-outline": "m7.056 2.418 1.167-.351a2.75 2.75 0 0 1 3.302 1.505l.902 2.006a2.75 2.75 0 0 1-.633 3.139L10.3 10.11a.25.25 0 0 0-.078.155c-.044.397.225 1.17.845 2.245.451.781.86 1.33 1.207 1.637.242.215.375.261.432.245l2.01-.615a2.75 2.75 0 0 1 3.034 1.02l1.281 1.776a2.75 2.75 0 0 1-.339 3.605l-.886.84a3.75 3.75 0 0 1-3.587.889c-2.754-.769-5.223-3.093-7.435-6.924-2.215-3.836-2.992-7.14-2.276-9.913a3.75 3.75 0 0 1 2.548-2.652Zm.433 1.437a2.25 2.25 0 0 0-1.529 1.59c-.602 2.332.087 5.261 2.123 8.788 2.033 3.522 4.222 5.582 6.54 6.23a2.25 2.25 0 0 0 2.151-.534l.887-.84a1.25 1.25 0 0 0 .154-1.639l-1.28-1.775a1.25 1.25 0 0 0-1.38-.464l-2.015.617c-1.17.348-2.232-.593-3.372-2.568C9 11.93 8.642 10.9 8.731 10.099c.047-.416.24-.8.546-1.086l1.494-1.393a1.25 1.25 0 0 0 .288-1.427l-.902-2.006a1.25 1.25 0 0 0-1.5-.684l-1.168.352Z",
|
||||
"chat-help-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5Zm0 12a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm0-8.75a2.75 2.75 0 0 1 2.75 2.75c0 1.01-.297 1.574-1.051 2.359l-.169.171c-.622.622-.78.886-.78 1.47a.75.75 0 0 1-1.5 0c0-1.01.297-1.574 1.051-2.359l.169-.171c.622-.622.78-.886.78-1.47a1.25 1.25 0 0 0-2.493-.128l-.007.128a.75.75 0 0 1-1.5 0A2.75 2.75 0 0 1 12 6.75Z",
|
||||
"chat-multiple-outline": "M9.562 3a7.5 7.5 0 0 0-6.798 10.673l-.724 2.842a1.25 1.25 0 0 0 1.504 1.524c.75-.18 1.903-.457 2.93-.702A7.5 7.5 0 1 0 9.561 3Zm-6 7.5a6 6 0 1 1 3.33 5.375l-.244-.121-.264.063c-.923.22-1.99.475-2.788.667l.69-2.708.07-.276-.13-.253a5.971 5.971 0 0 1-.664-2.747Zm11 10.5c-1.97 0-3.762-.759-5.1-2h.1c.718 0 1.415-.089 2.08-.257.865.482 1.86.757 2.92.757.96 0 1.866-.225 2.67-.625l.243-.121.264.063c.922.22 1.966.445 2.74.61-.175-.751-.414-1.756-.642-2.651l-.07-.276.13-.253a5.971 5.971 0 0 0 .665-2.747 5.995 5.995 0 0 0-2.747-5.042 8.44 8.44 0 0 0-.8-2.047 7.503 7.503 0 0 1 4.344 10.263c.253 1.008.509 2.1.671 2.803a1.244 1.244 0 0 1-1.467 1.5 132.62 132.62 0 0 1-2.913-.64 7.476 7.476 0 0 1-3.088.663Z",
|
||||
|
@ -35,6 +38,7 @@
|
|||
"dismiss-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm3.446 4.897.084.073a.75.75 0 0 1 .073.976l-.073.084L13.061 12l2.47 2.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-2.47 2.47a.75.75 0 0 1-.976.072l-.084-.073a.75.75 0 0 1-.073-.976l.073-.084L10.939 12l-2.47-2.47a.75.75 0 0 1-.072-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l2.47-2.47a.75.75 0 0 1 .976-.072Z",
|
||||
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
||||
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",
|
||||
"dual-screen-clock-outline": "M10.019 6.002a6.632 6.632 0 0 0 .058 1.5H3.75a.25.25 0 0 0-.25.25v12.494c0 .138.112.25.25.25h7.498l-.001-10.167c.416.57.924 1.07 1.5 1.479v8.69h7.498a.25.25 0 0 0 .25-.25v-8.62a6.535 6.535 0 0 0 1.501-1.656V20.25a1.75 1.75 0 0 1-1.75 1.75h-8.998l-.001-.003H3.75A1.75 1.75 0 0 1 2 20.246V7.751c0-.966.784-1.75 1.75-1.75h6.269Zm6.22 11.498a.75.75 0 0 1 .101 1.493L16.24 19h-1.5a.75.75 0 0 1-.102-1.493l.102-.007h1.5Zm-6.996 0a.75.75 0 0 1 .102 1.493L9.243 19H7.74a.75.75 0 0 1-.102-1.493l.102-.007h1.502ZM16.498 1a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-1 2a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 .5.5h3.001a.5.5 0 0 0 0-1h-2.501V3.5a.5.5 0 0 0-.5-.5Z",
|
||||
"edit-outline": "M21.03 2.97a3.578 3.578 0 0 1 0 5.06L9.062 20a2.25 2.25 0 0 1-.999.58l-5.116 1.395a.75.75 0 0 1-.92-.921l1.395-5.116a2.25 2.25 0 0 1 .58-.999L15.97 2.97a3.578 3.578 0 0 1 5.06 0ZM15 6.06 5.062 16a.75.75 0 0 0-.193.333l-1.05 3.85 3.85-1.05A.75.75 0 0 0 8 18.938L17.94 9 15 6.06Zm2.03-2.03-.97.97L19 7.94l.97-.97a2.079 2.079 0 0 0-2.94-2.94Z",
|
||||
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
|
||||
"error-circle-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z",
|
||||
|
@ -69,6 +73,7 @@
|
|||
"resize-large-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5A3.25 3.25 0 0 1 6.25 3h1.5a.75.75 0 0 1 0 1.5h-1.5ZM19.5 6.25a1.75 1.75 0 0 0-1.75-1.75h-1.5a.75.75 0 0 1 0-1.5h1.5A3.25 3.25 0 0 1 21 6.25v1.5a.75.75 0 0 1-1.5 0v-1.5ZM19.5 17.75a1.75 1.75 0 0 1-1.75 1.75h-1.5a.75.75 0 0 0 0 1.5h1.5A3.25 3.25 0 0 0 21 17.75v-1.5a.75.75 0 0 0-1.5 0v1.5ZM4.5 17.75c0 .966.784 1.75 1.75 1.75h1.5a.75.75 0 0 1 0 1.5h-1.5A3.25 3.25 0 0 1 3 17.75v-1.5a.75.75 0 0 1 1.5 0v1.5ZM8.25 6A2.25 2.25 0 0 0 6 8.25v7.5A2.25 2.25 0 0 0 8.25 18h7.5A2.25 2.25 0 0 0 18 15.75v-7.5A2.25 2.25 0 0 0 15.75 6h-7.5ZM7.5 8.25a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 .75.75v7.5a.75.75 0 0 1-.75.75h-7.5a.75.75 0 0 1-.75-.75v-7.5Z",
|
||||
"search-outline": "M10 2.75a7.25 7.25 0 0 1 5.63 11.819l4.9 4.9a.75.75 0 0 1-.976 1.134l-.084-.073-4.901-4.9A7.25 7.25 0 1 1 10 2.75Zm0 1.5a5.75 5.75 0 1 0 0 11.5 5.75 5.75 0 0 0 0-11.5Z",
|
||||
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
|
||||
"send-clock-outline": "M5.694 12 2.299 3.272c-.236-.608.356-1.189.942-.982l.093.04 18 9a.752.752 0 0 1 .264 1.124 6.473 6.473 0 0 0-4.272-1.452L4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .724.556c-.472.26-.909.578-1.3.944H7.011l-2.609 6.71 6.753-3.377a6.522 6.522 0 0 0-.147 1.75l-7.674 3.838c-.583.291-1.217-.245-1.065-.847l.03-.096L5.694 12ZM23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 1 1 0 1H17a.5.5 0 0 1-.5-.5v-3a.5.5 0 0 1 1 0v2.5Z",
|
||||
"settings-outline": "M12.012 2.25c.734.008 1.465.093 2.182.253a.75.75 0 0 1 .582.649l.17 1.527a1.384 1.384 0 0 0 1.927 1.116l1.401-.615a.75.75 0 0 1 .85.174 9.792 9.792 0 0 1 2.204 3.792.75.75 0 0 1-.271.825l-1.242.916a1.381 1.381 0 0 0 0 2.226l1.243.915a.75.75 0 0 1 .272.826 9.797 9.797 0 0 1-2.204 3.792.75.75 0 0 1-.848.175l-1.407-.617a1.38 1.38 0 0 0-1.926 1.114l-.169 1.526a.75.75 0 0 1-.572.647 9.518 9.518 0 0 1-4.406 0 .75.75 0 0 1-.572-.647l-.168-1.524a1.382 1.382 0 0 0-1.926-1.11l-1.406.616a.75.75 0 0 1-.849-.175 9.798 9.798 0 0 1-2.204-3.796.75.75 0 0 1 .272-.826l1.243-.916a1.38 1.38 0 0 0 0-2.226l-1.243-.914a.75.75 0 0 1-.271-.826 9.793 9.793 0 0 1 2.204-3.792.75.75 0 0 1 .85-.174l1.4.615a1.387 1.387 0 0 0 1.93-1.118l.17-1.526a.75.75 0 0 1 .583-.65c.717-.159 1.45-.243 2.201-.252Zm0 1.5a9.135 9.135 0 0 0-1.354.117l-.109.977A2.886 2.886 0 0 1 6.525 7.17l-.898-.394a8.293 8.293 0 0 0-1.348 2.317l.798.587a2.881 2.881 0 0 1 0 4.643l-.799.588c.32.842.776 1.626 1.348 2.322l.905-.397a2.882 2.882 0 0 1 4.017 2.318l.11.984c.889.15 1.798.15 2.687 0l.11-.984a2.881 2.881 0 0 1 4.018-2.322l.905.396a8.296 8.296 0 0 0 1.347-2.318l-.798-.588a2.881 2.881 0 0 1 0-4.643l.796-.587a8.293 8.293 0 0 0-1.348-2.317l-.896.393a2.884 2.884 0 0 1-4.023-2.324l-.11-.976a8.988 8.988 0 0 0-1.333-.117ZM12 8.25a3.75 3.75 0 1 1 0 7.5 3.75 3.75 0 0 1 0-7.5Zm0 1.5a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z",
|
||||
"share-outline": "M6.747 4h3.464a.75.75 0 0 1 .102 1.493l-.102.007H6.747a2.25 2.25 0 0 0-2.245 2.096l-.005.154v9.5a2.25 2.25 0 0 0 2.096 2.245l.154.005h9.5a2.25 2.25 0 0 0 2.245-2.096l.005-.154v-.498a.75.75 0 0 1 1.494-.101l.006.101v.498a3.75 3.75 0 0 1-3.55 3.745l-.2.005h-9.5a3.75 3.75 0 0 1-3.745-3.55l-.005-.2v-9.5a3.75 3.75 0 0 1 3.55-3.745l.2-.005h3.464-3.464ZM14.5 6.52V3.75a.75.75 0 0 1 1.187-.61l.082.069 5.994 5.75c.28.268.306.7.077.997l-.077.085-5.994 5.752a.75.75 0 0 1-1.262-.434l-.007-.107v-2.725l-.344.03c-2.4.25-4.7 1.33-6.914 3.26-.52.453-1.323.025-1.237-.658.664-5.32 3.446-8.252 8.195-8.62l.3-.02V3.75v2.77ZM16 5.509V7.25a.75.75 0 0 1-.75.75c-3.874 0-6.274 1.676-7.312 5.157l-.079.279.352-.237C10.45 11.737 12.798 11 15.251 11a.75.75 0 0 1 .743.648l.007.102v1.743L20.16 9.5l-4.16-3.991Z",
|
||||
"sound-source-outline": "M3.5 12a8.5 8.5 0 1 1 14.762 5.748l.992 1.135A9.966 9.966 0 0 0 22 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.966 9.966 0 0 0 2.746 6.883l.993-1.134A8.47 8.47 0 0 1 3.5 12Z M19.25 12.125a7.098 7.098 0 0 1-1.783 4.715l-.998-1.14a5.625 5.625 0 1 0-8.806-.15l-1.004 1.146a7.125 7.125 0 1 1 12.59-4.571Z M16.25 12a4.23 4.23 0 0 1-.821 2.511l-1.026-1.172a2.75 2.75 0 1 0-4.806 0L8.571 14.51A4.25 4.25 0 1 1 16.25 12Z M12.564 12.756a.75.75 0 0 0-1.128 0l-7 8A.75.75 0 0 0 5 22h14a.75.75 0 0 0 .564-1.244l-7-8Zm4.783 7.744H6.653L12 14.389l5.347 6.111Z",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
|
||||
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
|
||||
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
|
||||
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
|
||||
|
|
|
@ -56,7 +56,7 @@ export default {
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
value: function(newValue) {
|
||||
value(newValue) {
|
||||
this.greetingsMessage = newValue;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<template>
|
||||
<li class="sub-menu-container">
|
||||
<div class="sub-menu-title">
|
||||
<span class="small">{{ title }}</span>
|
||||
</div>
|
||||
<ul class="sub-menu-li-container">
|
||||
<woot-dropdown-header :title="title" />
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||
|
||||
export default {
|
||||
name: 'WootDropdownMenu',
|
||||
componentName: 'WootDropdownMenu',
|
||||
|
||||
components: {
|
||||
WootDropdownHeader,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
|
@ -23,21 +26,7 @@ export default {
|
|||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sub-menu-container {
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: var(--space-micro);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
.sub-menu-title {
|
||||
padding: var(--space-one) var(--space-one) var(--space-smaller);
|
||||
text-transform: uppercase;
|
||||
|
||||
.small {
|
||||
color: var(--b-600);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu-li-container {
|
||||
|
|
|
@ -13,6 +13,17 @@ export const MESSAGE_TYPE = {
|
|||
// Size in mega bytes
|
||||
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
||||
|
||||
export const ALLOWED_FILE_TYPES =
|
||||
'image/*,' +
|
||||
'audio/*,' +
|
||||
'video/*,' +
|
||||
'.3gpp,' +
|
||||
'text/csv, text/plain, application/json, application/pdf, text/rtf,' +
|
||||
'application/zip, application/x-7z-compressed application/vnd.rar application/x-tar,' +
|
||||
'application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/vnd.oasis.opendocument.text,' +
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,' +
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document,';
|
||||
|
||||
export const CSAT_RATINGS = [
|
||||
{
|
||||
key: 'disappointed',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.file-uploads .attachment-button + label {
|
||||
.file-uploads .attachment-button+label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
|
||||
.agent-message-wrap {
|
||||
+ .agent-message-wrap {
|
||||
+.agent-message-wrap {
|
||||
margin-top: $space-micro;
|
||||
|
||||
.agent-message .chat-bubble {
|
||||
|
@ -67,11 +67,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
+ .user-message-wrap {
|
||||
+.user-message-wrap {
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
|
||||
&.has-response + .user-message-wrap {
|
||||
&.has-response+.user-message-wrap {
|
||||
margin-top: $space-micro;
|
||||
|
||||
.chat-bubble {
|
||||
|
@ -79,7 +79,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.has-response + .agent-message-wrap {
|
||||
&.has-response+.agent-message-wrap {
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
}
|
||||
|
@ -98,9 +98,21 @@
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.in-progress {
|
||||
.in-progress,
|
||||
.is-failed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.is-failed {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.chat-bubble.user {
|
||||
background: $color-error !important;
|
||||
// TODO: Remove the important
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -115,7 +127,7 @@
|
|||
}
|
||||
|
||||
.user-message-wrap {
|
||||
+ .user-message-wrap {
|
||||
+.user-message-wrap {
|
||||
margin-top: $space-micro;
|
||||
|
||||
.user-message .chat-bubble {
|
||||
|
@ -123,7 +135,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
+ .agent-message-wrap {
|
||||
+.agent-message-wrap {
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +166,7 @@
|
|||
border: 1px solid $color-border-dark;
|
||||
}
|
||||
|
||||
+ .chat-bubble-wrap {
|
||||
+.chat-bubble-wrap {
|
||||
.chat-bubble {
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
|
@ -176,7 +188,7 @@
|
|||
border-radius: $space-two;
|
||||
}
|
||||
|
||||
+ .chat-bubble-wrap {
|
||||
+.chat-bubble-wrap {
|
||||
.chat-bubble {
|
||||
border-top-right-radius: $space-smaller;
|
||||
}
|
||||
|
@ -206,7 +218,7 @@
|
|||
text-align: left;
|
||||
word-break: break-word;
|
||||
|
||||
> a {
|
||||
>a {
|
||||
color: $color-primary;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
@ -218,7 +230,7 @@
|
|||
&.user {
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
|
||||
> a {
|
||||
>a {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,16 +27,14 @@
|
|||
:class="wrapClass"
|
||||
>
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||
<file-bubble
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||
:url="attachment.data_url"
|
||||
:thumb="attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<file-bubble v-else :url="attachment.data_url" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
||||
|
@ -83,6 +81,11 @@ export default {
|
|||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldDisplayAgentMessage() {
|
||||
if (
|
||||
|
@ -170,5 +173,18 @@ export default {
|
|||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<file-upload
|
||||
:size="4096 * 2048"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
:accept="allowedFileTypes"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<button class="icon-button flex items-center justify-center">
|
||||
|
@ -15,7 +15,10 @@
|
|||
import FileUpload from 'vue-upload-component';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||
import {
|
||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
ALLOWED_FILE_TYPES,
|
||||
} from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
export default {
|
||||
|
@ -33,6 +36,9 @@ export default {
|
|||
fileUploadSizeLimit() {
|
||||
return MAXIMUM_FILE_UPLOAD_SIZE;
|
||||
},
|
||||
allowedFileTypes() {
|
||||
return ALLOWED_FILE_TYPES;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFileType(fileType) {
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
class="image"
|
||||
>
|
||||
<div class="wrap">
|
||||
<img :src="thumb" alt="Picture message" />
|
||||
<img
|
||||
:src="thumb"
|
||||
alt="Picture message"
|
||||
@click="onClick"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -14,7 +19,16 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ['url', 'thumb', 'readableTime'],
|
||||
props: {
|
||||
url: { type: String, default: '' },
|
||||
thumb: { type: String, default: '' },
|
||||
readableTime: { type: String, default: '' },
|
||||
},
|
||||
methods: {
|
||||
onImgError() {
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -11,9 +11,11 @@ export default {
|
|||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<template>
|
||||
<div class="user-message-wrap">
|
||||
<div class="user-message">
|
||||
<div class="message-wrap" :class="{ 'in-progress': isInProgress }">
|
||||
<div
|
||||
class="message-wrap"
|
||||
:class="{ 'in-progress': isInProgress, 'is-failed': isFailed }"
|
||||
>
|
||||
<user-message-bubble
|
||||
v-if="showTextBubble"
|
||||
:message="message.content"
|
||||
|
@ -14,19 +17,33 @@
|
|||
:style="{ backgroundColor: widgetColor }"
|
||||
>
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||
<file-bubble
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
:is-in-progress="isInProgress"
|
||||
/>
|
||||
<image-bubble
|
||||
v-else
|
||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||
:url="attachment.data_url"
|
||||
:thumb="attachment.thumb_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<file-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
:is-in-progress="isInProgress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isFailed"
|
||||
class="flex justify-end align-middle px-4 py-2 text-red-700"
|
||||
>
|
||||
<button
|
||||
v-if="!hasAttachments"
|
||||
:title="$t('COMPONENTS.MESSAGE_BUBBLE.RETRY')"
|
||||
class="inline-flex justify-center items-center ml-2"
|
||||
@click="retrySendMessage"
|
||||
>
|
||||
<fluent-icon icon="arrow-clockwise" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,6 +52,7 @@
|
|||
<script>
|
||||
import UserMessageBubble from 'widget/components/UserMessageBubble';
|
||||
import ImageBubble from 'widget/components/ImageBubble';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index';
|
||||
import FileBubble from 'widget/components/FileBubble';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import messageMixin from '../mixins/messageMixin';
|
||||
|
@ -46,6 +64,7 @@ export default {
|
|||
UserMessageBubble,
|
||||
ImageBubble,
|
||||
FileBubble,
|
||||
FluentIcon,
|
||||
},
|
||||
mixins: [timeMixin, messageMixin],
|
||||
props: {
|
||||
|
@ -54,6 +73,11 @@ export default {
|
|||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
widgetColor: 'appConfig/getWidgetColor',
|
||||
|
@ -71,6 +95,35 @@ export default {
|
|||
const { created_at: createdAt = '' } = this.message;
|
||||
return this.messageStamp(createdAt);
|
||||
},
|
||||
isFailed() {
|
||||
const { status = '' } = this.message;
|
||||
return status === 'failed';
|
||||
},
|
||||
errorMessage() {
|
||||
const { meta } = this.message;
|
||||
return meta
|
||||
? meta.error
|
||||
: this.$t('COMPONENTS.MESSAGE_BUBBLE.ERROR_MESSAGE');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
},
|
||||
methods: {
|
||||
async retrySendMessage() {
|
||||
await this.$store.dispatch(
|
||||
'conversation/sendMessageWithData',
|
||||
this.message
|
||||
);
|
||||
},
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
},
|
||||
"FORM_BUBBLE": {
|
||||
"SUBMIT": "Submit"
|
||||
},
|
||||
"MESSAGE_BUBBLE": {
|
||||
"RETRY": "Send message again",
|
||||
"ERROR_MESSAGE": "Couldn't send, try again"
|
||||
}
|
||||
},
|
||||
"TEAM_AVAILABILITY": {
|
||||
|
|
|
@ -24,15 +24,35 @@ export const actions = {
|
|||
commit('setConversationUIFlag', { isCreating: false });
|
||||
}
|
||||
},
|
||||
sendMessage: async ({ commit }, params) => {
|
||||
sendMessage: async ({ dispatch }, params) => {
|
||||
const { content } = params;
|
||||
commit('pushMessageToConversation', createTemporaryMessage({ content }));
|
||||
await sendMessageAPI(content);
|
||||
const message = createTemporaryMessage({ content });
|
||||
|
||||
dispatch('sendMessageWithData', message);
|
||||
},
|
||||
sendMessageWithData: async ({ commit }, message) => {
|
||||
const { id, content, meta = {} } = message;
|
||||
|
||||
commit('pushMessageToConversation', message);
|
||||
commit('updateMessageMeta', { id, meta: { ...meta, error: '' } });
|
||||
try {
|
||||
const { data } = await sendMessageAPI(content);
|
||||
|
||||
commit('deleteMessage', message.id);
|
||||
commit('pushMessageToConversation', { ...data, status: 'sent' });
|
||||
} catch (error) {
|
||||
commit('pushMessageToConversation', { ...message, status: 'failed' });
|
||||
commit('updateMessageMeta', {
|
||||
id,
|
||||
meta: { ...meta, error: '' },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
sendAttachment: async ({ commit }, params) => {
|
||||
const {
|
||||
attachment: { thumbUrl, fileType },
|
||||
meta = {},
|
||||
} = params;
|
||||
const attachment = {
|
||||
thumb_url: thumbUrl,
|
||||
|
@ -50,7 +70,13 @@ export const actions = {
|
|||
message: data,
|
||||
tempId: tempMessage.id,
|
||||
});
|
||||
commit('pushMessageToConversation', { ...data, status: 'sent' });
|
||||
} catch (error) {
|
||||
commit('pushMessageToConversation', { ...tempMessage, status: 'failed' });
|
||||
commit('updateMessageMeta', {
|
||||
id: tempMessage.id,
|
||||
meta: { ...meta, error: '' },
|
||||
});
|
||||
// Show error
|
||||
}
|
||||
},
|
||||
|
|
|
@ -72,6 +72,16 @@ export const mutations = {
|
|||
};
|
||||
},
|
||||
|
||||
updateMessageMeta($state, { id, meta }) {
|
||||
const message = $state.conversations[id];
|
||||
if (!message) return;
|
||||
|
||||
const newMeta = message.meta ? { ...message.meta, ...meta } : { ...meta };
|
||||
Vue.set(message, 'meta', {
|
||||
...newMeta,
|
||||
});
|
||||
},
|
||||
|
||||
deleteMessage($state, id) {
|
||||
const messagesInbox = $state.conversations;
|
||||
Vue.delete(messagesInbox, id);
|
||||
|
|
|
@ -6,6 +6,7 @@ jest.mock('../../../../helpers/uuid');
|
|||
jest.mock('widget/helpers/axios');
|
||||
|
||||
const commit = jest.fn();
|
||||
const dispatch = jest.fn();
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#createConversation', () => {
|
||||
|
@ -92,7 +93,7 @@ describe('#actions', () => {
|
|||
});
|
||||
|
||||
describe('#sendMessage', () => {
|
||||
it('sends correct mutations', () => {
|
||||
it('sends correct mutations', async () => {
|
||||
const mockDate = new Date(1466424490000);
|
||||
getUuid.mockImplementationOnce(() => '1111');
|
||||
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||
|
@ -109,15 +110,16 @@ describe('#actions', () => {
|
|||
search: '?param=1',
|
||||
},
|
||||
}));
|
||||
actions.sendMessage({ commit }, { content: 'hello' });
|
||||
await actions.sendMessage({ commit, dispatch }, { content: 'hello' });
|
||||
spy.mockRestore();
|
||||
windowSpy.mockRestore();
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', {
|
||||
id: '1111',
|
||||
expect(dispatch).toBeCalledWith('sendMessageWithData', {
|
||||
attachments: undefined,
|
||||
content: 'hello',
|
||||
status: 'in_progress',
|
||||
created_at: 1466424490,
|
||||
id: '1111',
|
||||
message_type: 0,
|
||||
status: 'in_progress',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -130,7 +132,7 @@ describe('#actions', () => {
|
|||
const thumbUrl = '';
|
||||
const attachment = { thumbUrl, fileType: 'file' };
|
||||
|
||||
actions.sendAttachment({ commit }, { attachment });
|
||||
actions.sendAttachment({ commit, dispatch }, { attachment });
|
||||
spy.mockRestore();
|
||||
expect(commit).toBeCalledWith('pushMessageToConversation', {
|
||||
id: '1111',
|
||||
|
|
|
@ -3,7 +3,7 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
|
|||
|
||||
def perform
|
||||
Inbox.where(channel_type: 'Channel::Email').all.each do |inbox|
|
||||
Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
|
||||
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ class NotificationListener < BaseListener
|
|||
def assignee_changed(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
assignee = conversation.assignee
|
||||
return unless conversation.notifiable_assignee_change?
|
||||
return if event.data[:notifiable_assignee_change].blank?
|
||||
return if conversation.pending?
|
||||
|
||||
NotificationBuilder.new(
|
||||
|
|
|
@ -63,14 +63,19 @@ class ReplyMailbox < ApplicationMailbox
|
|||
# find conversation uuid from below pattern
|
||||
# <conversation/#{@conversation.uuid}/messages/#{@messages&.last&.id}@#{@account.inbound_email_domain}>
|
||||
def find_conversation_with_in_reply_to
|
||||
in_reply_to = mail.in_reply_to
|
||||
match_result = in_reply_to.match(ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN) if in_reply_to.present?
|
||||
|
||||
if match_result
|
||||
find_conversation_by_uuid(match_result)
|
||||
else
|
||||
find_conversation_by_message_id(in_reply_to)
|
||||
match_result = nil
|
||||
in_reply_to_addresses = mail.in_reply_to
|
||||
in_reply_to_addresses = [in_reply_to_addresses] if in_reply_to_addresses.is_a?(String)
|
||||
in_reply_to_addresses.each do |in_reply_to|
|
||||
match_result = in_reply_to.match(::ApplicationMailbox::CONVERSATION_MESSAGE_ID_PATTERN)
|
||||
break if match_result
|
||||
end
|
||||
find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
|
||||
end
|
||||
|
||||
def find_by_in_reply_to_addresses(match_result, in_reply_to_addresses)
|
||||
find_conversation_by_uuid(match_result) if match_result
|
||||
find_conversation_by_message_id(in_reply_to_addresses) if @conversation.blank?
|
||||
end
|
||||
|
||||
def verify_decoded_params
|
||||
|
|
|
@ -17,6 +17,18 @@
|
|||
|
||||
class Attachment < ApplicationRecord
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
ACCEPTABLE_FILE_TYPES = %w[
|
||||
text/csv text/plain text/rtf
|
||||
application/json application/pdf
|
||||
application/zip application/x-7z-compressed application/vnd.rar application/x-tar
|
||||
application/msword application/vnd.ms-excel application/vnd.ms-powerpoint application/rtf
|
||||
application/vnd.oasis.opendocument.text
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
].freeze
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :message
|
||||
has_one_attached :file
|
||||
|
@ -90,10 +102,19 @@ class Attachment < ApplicationRecord
|
|||
def acceptable_file
|
||||
return unless should_validate_file?
|
||||
|
||||
errors.add(:file, 'is too big') if file.byte_size > 40.megabytes
|
||||
validate_file_size(file.byte_size)
|
||||
validate_file_content_type(file.content_type)
|
||||
end
|
||||
|
||||
acceptable_types = ['image/png', 'image/jpeg', 'image/gif', 'image/bmp', 'image/tiff', 'application/pdf', 'audio/mpeg', 'video/mp4', 'audio/ogg',
|
||||
'text/csv'].freeze
|
||||
errors.add(:file, 'filetype not supported') unless acceptable_types.include?(file.content_type)
|
||||
def validate_file_content_type(file_content_type)
|
||||
errors.add(:file, 'type not supported') unless media_file?(file_content_type) || ACCEPTABLE_FILE_TYPES.include?(file_content_type)
|
||||
end
|
||||
|
||||
def validate_file_size(byte_size)
|
||||
errors.add(:file, 'size is too big') if byte_size > 40.megabytes
|
||||
end
|
||||
|
||||
def media_file?(file_content_type)
|
||||
file_content_type.start_with?('image/', 'video/', 'audio/')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ module ActivityMessageHandler
|
|||
I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration)
|
||||
end
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def create_label_added(user_name, labels = [])
|
||||
|
@ -33,7 +33,7 @@ module ActivityMessageHandler
|
|||
params = { user_name: user_name, labels: labels.join(', ') }
|
||||
content = I18n.t('conversations.activity.labels.added', **params)
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def create_label_removed(user_name, labels = [])
|
||||
|
@ -42,7 +42,7 @@ module ActivityMessageHandler
|
|||
params = { user_name: user_name, labels: labels.join(', ') }
|
||||
content = I18n.t('conversations.activity.labels.removed', **params)
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def create_muted_message
|
||||
|
@ -51,7 +51,7 @@ module ActivityMessageHandler
|
|||
params = { user_name: Current.user.name }
|
||||
content = I18n.t('conversations.activity.muted', **params)
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def create_unmuted_message
|
||||
|
@ -60,7 +60,7 @@ module ActivityMessageHandler
|
|||
params = { user_name: Current.user.name }
|
||||
content = I18n.t('conversations.activity.unmuted', **params)
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def generate_team_change_activity_key
|
||||
|
@ -82,7 +82,7 @@ module ActivityMessageHandler
|
|||
params[:team_name] = generate_team_name_for_activity if key == 'removed'
|
||||
content = I18n.t("conversations.activity.team.#{key}", **params)
|
||||
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
|
||||
def generate_assignee_change_activity_content(user_name)
|
||||
|
@ -96,6 +96,6 @@ module ActivityMessageHandler
|
|||
return unless user_name
|
||||
|
||||
content = generate_assignee_change_activity_content(user_name)
|
||||
Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content
|
||||
end
|
||||
end
|
||||
|
|
|
@ -142,9 +142,9 @@ class Conversation < ApplicationRecord
|
|||
end
|
||||
|
||||
def notifiable_assignee_change?
|
||||
return false if self_assign?(assignee_id)
|
||||
return false unless saved_change_to_assignee_id?
|
||||
return false if assignee_id.blank?
|
||||
return false if self_assign?(assignee_id)
|
||||
|
||||
true
|
||||
end
|
||||
|
@ -202,7 +202,7 @@ class Conversation < ApplicationRecord
|
|||
end
|
||||
|
||||
def dispatcher_dispatch(event_name)
|
||||
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self)
|
||||
Rails.configuration.dispatcher.dispatch(event_name, Time.zone.now, conversation: self, notifiable_assignee_change: notifiable_assignee_change?)
|
||||
end
|
||||
|
||||
def conversation_status_changed_to_open?
|
||||
|
|
|
@ -10,12 +10,14 @@
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_inbox_members_on_inbox_id (inbox_id)
|
||||
# index_inbox_members_on_inbox_id (inbox_id)
|
||||
# index_inbox_members_on_inbox_id_and_user_id (inbox_id,user_id) UNIQUE
|
||||
#
|
||||
|
||||
class InboxMember < ApplicationRecord
|
||||
validates :inbox_id, presence: true
|
||||
validates :user_id, presence: true
|
||||
validates :user_id, uniqueness: { scope: :inbox_id }
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :inbox
|
||||
|
|
|
@ -66,7 +66,7 @@ class Notification < ApplicationRecord
|
|||
notification_type: notification_type,
|
||||
primary_actor_id: primary_actor_id,
|
||||
primary_actor_type: primary_actor_type,
|
||||
primary_actor: primary_actor.push_event_data.slice(:conversation_id)
|
||||
primary_actor: primary_actor.push_event_data.with_indifferent_access.slice('conversation_id', 'id')
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -22,4 +22,5 @@
|
|||
class TeamMember < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :team
|
||||
validates :user_id, uniqueness: { scope: :team_id }
|
||||
end
|
||||
|
|
31
app/presenters/html_parser.rb
Normal file
31
app/presenters/html_parser.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
class HtmlParser
|
||||
def self.parse_reply(raw_body)
|
||||
new(raw_body).filtered_text
|
||||
end
|
||||
|
||||
attr_reader :raw_body
|
||||
|
||||
def initialize(raw_body)
|
||||
@raw_body = raw_body
|
||||
end
|
||||
|
||||
def document
|
||||
@document ||= Nokogiri::HTML(raw_body)
|
||||
end
|
||||
|
||||
def filter_replies!
|
||||
document.xpath('//blockquote').each { |n| n.replace('> ') }
|
||||
document.xpath('//table').each(&:remove)
|
||||
end
|
||||
|
||||
def filtered_html
|
||||
@filtered_html ||= begin
|
||||
filter_replies!
|
||||
document.inner_html
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_text
|
||||
@filtered_text ||= Html2Text.convert(filtered_html)
|
||||
end
|
||||
end
|
|
@ -117,37 +117,10 @@ class MailPresenter < SimpleDelegator
|
|||
end
|
||||
|
||||
def extract_reply(content)
|
||||
@regex_arr ||= quoted_text_regexes
|
||||
|
||||
content_length = content.length
|
||||
# calculates the matching regex closest to top of page
|
||||
index = @regex_arr.inject(content_length) do |min, regex|
|
||||
[(content.index(regex) || content_length), min].min
|
||||
end
|
||||
|
||||
# NOTE: implement the reply parser over here
|
||||
{
|
||||
reply: content[0..(index - 1)].strip,
|
||||
quoted_text: content[index..].strip
|
||||
reply: content.strip,
|
||||
quoted_text: content.strip
|
||||
}
|
||||
end
|
||||
|
||||
def quoted_text_regexes
|
||||
return sender_agnostic_regexes if @account.nil? || @account.support_email.blank?
|
||||
|
||||
[
|
||||
Regexp.new("From:\s* #{Regexp.escape(@account.support_email)}", Regexp::IGNORECASE),
|
||||
Regexp.new("<#{Regexp.escape(@account.support_email)}>", Regexp::IGNORECASE),
|
||||
Regexp.new("#{Regexp.escape(@account.support_email)}\s+wrote:", Regexp::IGNORECASE),
|
||||
Regexp.new("On(.*)#{Regexp.escape(@account.support_email)}(.*)wrote:", Regexp::IGNORECASE)
|
||||
] + sender_agnostic_regexes
|
||||
end
|
||||
|
||||
def sender_agnostic_regexes
|
||||
@sender_agnostic_regexes ||= [
|
||||
Regexp.new("^.*On.*(\n)?wrote:$", Regexp::IGNORECASE),
|
||||
Regexp.new('^.*On(.*)(.*)wrote:$', Regexp::IGNORECASE),
|
||||
Regexp.new("-+original\s+message-+\s*$", Regexp::IGNORECASE),
|
||||
Regexp.new("from:\s*$", Regexp::IGNORECASE)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
shared: &shared
|
||||
version: '2.0.0'
|
||||
version: '2.0.2'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# ref: https://dev.to/nodefiend/rails-migration-adding-a-unique-index-and-deleting-duplicates-5cde
|
||||
|
||||
class AddUniqueIndexOnInboxMembers < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
# partioning the duplicate records and then removing where more than one row is found
|
||||
ActiveRecord::Base.connection.execute('
|
||||
DELETE FROM inbox_members WHERE id IN (SELECT id from (
|
||||
SELECT id, user_id, inbox_id, ROW_NUMBER() OVER w AS rnum FROM inbox_members WINDOW w AS (
|
||||
PARTITION BY inbox_id, user_id ORDER BY id
|
||||
)
|
||||
) t WHERE t.rnum > 1)
|
||||
')
|
||||
add_index :inbox_members, [:inbox_id, :user_id], unique: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_index :inbox_members, [:inbox_id, :user_id]
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_12_19_031453) do
|
||||
ActiveRecord::Schema.define(version: 2021_12_21_125545) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
@ -429,6 +429,7 @@ ActiveRecord::Schema.define(version: 2021_12_19_031453) do
|
|||
t.integer "inbox_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["inbox_id", "user_id"], name: "index_inbox_members_on_inbox_id_and_user_id", unique: true
|
||||
t.index ["inbox_id"], name: "index_inbox_members_on_inbox_id"
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ module ExceptionList
|
|||
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
|
||||
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
|
||||
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout,
|
||||
RestClient::TemporaryRedirect, RestClient::SSLCertificateNotVerified, RestClient::PaymentRequired,
|
||||
RestClient::BadGateway, RestClient::Unauthorized, RestClient::PayloadTooLarge,
|
||||
RestClient::MovedPermanently, RestClient::ServiceUnavailable, Errno::ECONNREFUSED, SocketError].freeze
|
||||
SMTP_EXCEPTIONS = [
|
||||
Net::SMTPSyntaxError
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue} --fix",
|
||||
|
@ -51,7 +51,6 @@
|
|||
"semver": "7.3.5",
|
||||
"spinkit": "~1.2.5",
|
||||
"tailwindcss": "^1.9.6",
|
||||
"tween.js": "~16.6.0",
|
||||
"url-loader": "^2.0.0",
|
||||
"v-tooltip": "~2.1.3",
|
||||
"vue": "2.6.12",
|
||||
|
|
|
@ -81,7 +81,7 @@ RSpec.describe 'Inbox Member API', type: :request do
|
|||
end
|
||||
|
||||
it 'add inbox members' do
|
||||
params = { inbox_id: inbox.id, user_ids: [agent_to_add.id] }
|
||||
params = { inbox_id: inbox.id, user_ids: [old_agent.id, agent_to_add.id] }
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inbox_members",
|
||||
headers: administrator.create_new_auth_token,
|
||||
|
|
|
@ -55,6 +55,8 @@ RSpec.describe 'Team Members API', type: :request do
|
|||
it 'add a new team members when its administrator' do
|
||||
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
|
||||
params = { user_ids: user_ids }
|
||||
# have a team member added already
|
||||
create(:team_member, team: team, user: User.find(user_ids.first))
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
|
||||
params: params,
|
||||
|
@ -63,7 +65,7 @@ RSpec.describe 'Team Members API', type: :request do
|
|||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response.count).to eq(user_ids.count)
|
||||
expect(json_response.count).to eq(user_ids.count - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -93,15 +95,16 @@ RSpec.describe 'Team Members API', type: :request do
|
|||
|
||||
it 'destroys the team members when its administrator' do
|
||||
user_ids = (1..5).map { create(:user, account: account, role: :agent).id }
|
||||
params = { user_ids: user_ids }
|
||||
user_ids.each { |id| create(:team_member, team: team, user: User.find(id)) }
|
||||
params = { user_ids: user_ids.first(3) }
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}",
|
||||
delete "/api/v1/accounts/#{account.id}/teams/#{team.id}/team_members",
|
||||
params: params,
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(team.team_members.count).to eq(0)
|
||||
expect(team.team_members.count).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
2
spec/fixtures/files/forwarder_email.eml
vendored
2
spec/fixtures/files/forwarder_email.eml
vendored
|
@ -4,7 +4,7 @@ Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE
|
|||
Subject: Discussion: Let's debate these attachments
|
||||
Date: Tue, 20 Apr 2020 04:20:20 -0400
|
||||
X-Forwarded-For: reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com
|
||||
In-Reply-To: <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
|
||||
In-Reply-To: <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com> <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
|
||||
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
|
||||
X-Mailer: Apple Mail (2.1244.3)
|
||||
|
|
47
spec/fixtures/files/mail_with_quote.eml
vendored
Normal file
47
spec/fixtures/files/mail_with_quote.eml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
MIME-Version: 1.0
|
||||
Date: Thu, 19 Aug 2021 14:14:31 +0530
|
||||
References: <CAFkiBVxGoURoqdkY-O_25F-8b41kb-GWBc6hh4Djd5ynwOikXA@mail.gmail.com> <0100017b5d8efc70-c7f18809-aa55-48f6-91fd-b626092ed8b3-000000@email.amazonses.com>
|
||||
In-Reply-To: <0100017b5d8efc70-c7f18809-aa55-48f6-91fd-b626092ed8b3-000000@email.amazonses.com>
|
||||
Message-ID: <CAFkiBVwJjO_k_e-LpiKi7MAQAKbHX5nkEPcf0y1R=bjcEHogMg@mail.gmail.com>
|
||||
Subject: Re: Checking mail forwarding to cw inbox
|
||||
From: Sony Mathew <sony@chatwoot.com>
|
||||
To: Tejaswini <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com>
|
||||
Content-Type: multipart/alternative; boundary="0000000000004af64505c9e58f03"
|
||||
|
||||
--0000000000004af64505c9e58f03
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
Yes, I am providing you step how to reproduce this issue
|
||||
|
||||
On Thu, Aug 19, 2021 at 2:07 PM Tejaswini from Email sender test <
|
||||
tejaswini@chatwoot.com> wrote:
|
||||
|
||||
> Any update on this?
|
||||
>
|
||||
>
|
||||
|
||||
--
|
||||
* Sony Mathew*
|
||||
Software developer
|
||||
*Mob:9999999999
|
||||
|
||||
--0000000000004af64505c9e58f03
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<div dir=3D"ltr">Yes, I am providing you step how to reproduce this issue</=
|
||||
div><br><div class=3D"gmail_quote"><div dir=3D"ltr" class=3D"gmail_attr">On=
|
||||
Thu, Aug 19, 2021 at 2:07 PM Tejaswini from Email sender test &l=
|
||||
t;<a href=3D"mailto:tejaswini@chatwoot.com">tejaswini@chatwoot.com</a>> wrot=
|
||||
e:<br></div><blockquote class=3D"gmail_quote" style=3D"margin:0px 0px 0px 0=
|
||||
.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"> <p>
|
||||
</p><p>Any update on this?</p>
|
||||
|
||||
<p></p>
|
||||
</blockquote></div><br clear=3D"all"><div><br></div>-- <br><div dir=3D"ltr"=
|
||||
class=3D"gmail_signature"><div dir=3D"ltr"><div><div dir=3D"ltr"><div><div=
|
||||
><b>Sony Mathew.</b><br></div><span style=3D"font-family:"times ne=
|
||||
w roman",serif"><span></span><span></span>Software developer</span><br=
|
||||
></div><b>Mob:9999999999</b></div></div></div></div>
|
||||
|
||||
--0000000000004af64505c9e58f03--
|
4
spec/fixtures/files/reply.eml
vendored
4
spec/fixtures/files/reply.eml
vendored
|
@ -3,7 +3,7 @@ Mime-Version: 1.0 (Apple Message framework v1244.3)
|
|||
Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
|
||||
Subject: Discussion: Let's debate these attachments
|
||||
Date: Tue, 20 Apr 2020 04:20:20 -0400
|
||||
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>, <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||
To: "Replies" <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com>
|
||||
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
|
||||
|
@ -628,4 +628,4 @@ r/Fxp3Y0d2/4tvsR95f/AAH7Gvwmn9rUHsOgUCg//9k=
|
|||
|
||||
--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1--
|
||||
|
||||
--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--
|
||||
--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--
|
||||
|
|
|
@ -3,7 +3,7 @@ Mime-Version: 1.0 (Apple Message framework v1244.3)
|
|||
Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
|
||||
Subject: Discussion: Let's debate these attachments
|
||||
Date: Tue, 20 Apr 2020 04:20:20 -0400
|
||||
In-Reply-To: <account/1/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com>
|
||||
In-Reply-To: <account/1/conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489@test.com> <conversation/6bdc3f4d-0bec-4515-a284-5d916fdde489/messages/123@test.com>
|
||||
To: "Replies" <test@example.com>
|
||||
References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||
Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@chatwoot.com>
|
||||
|
|
1061
spec/fixtures/files/welcome_html.eml
vendored
Normal file
1061
spec/fixtures/files/welcome_html.eml
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -7,6 +7,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
|
|||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
|
||||
let(:reply_mail) { create_inbound_email_from_fixture('reply.eml') }
|
||||
let(:mail_with_quote) { create_inbound_email_from_fixture('mail_with_quote.eml') }
|
||||
let(:conversation) { create(:conversation, assignee: agent, inbox: create(:inbox, account: account, greeting_enabled: false), account: account) }
|
||||
let(:described_subject) { described_class.receive reply_mail }
|
||||
let(:serialized_attributes) do
|
||||
|
|
|
@ -78,19 +78,11 @@ shared_examples_for 'assignment_handler' do
|
|||
expect(conversation.reload.assignee).to eq(agent)
|
||||
end
|
||||
|
||||
it 'creates a new notification for the agent' do
|
||||
it 'dispaches assignee changed event' do
|
||||
# TODO: FIX me
|
||||
# expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once).with('assignee.changed', anything, anything, anything, anything))
|
||||
expect(EventDispatcherJob).to(have_been_enqueued.at_least(:once))
|
||||
expect(update_assignee).to eq(true)
|
||||
expect(agent.notifications.count).to eq(1)
|
||||
end
|
||||
|
||||
it 'does not create assignment notification if notification setting is turned off' do
|
||||
notification_setting = agent.notification_settings.first
|
||||
notification_setting.unselect_all_email_flags
|
||||
notification_setting.unselect_all_push_flags
|
||||
notification_setting.save!
|
||||
|
||||
expect(update_assignee).to eq(true)
|
||||
expect(agent.notifications.count).to eq(0)
|
||||
end
|
||||
|
||||
context 'when agent is current user' do
|
||||
|
|
|
@ -54,7 +54,7 @@ RSpec.describe Conversation, type: :model do
|
|||
it 'runs after_create callbacks' do
|
||||
# send_events
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation)
|
||||
.with(described_class::CONVERSATION_CREATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -85,11 +85,11 @@ RSpec.describe Conversation, type: :model do
|
|||
label_list: [label.title]
|
||||
)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation)
|
||||
.with(described_class::CONVERSATION_RESOLVED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation)
|
||||
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation)
|
||||
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'creates conversation activities' do
|
||||
|
|
|
@ -78,4 +78,23 @@ RSpec.describe Notification do
|
|||
expect(notification.push_message_title).to eq "[##{message.conversation.display_id}] Hey @John Peter please check this?"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when fcm push data' do
|
||||
it 'returns correct data for primary actor conversation' do
|
||||
notification = create(:notification, notification_type: 'conversation_creation')
|
||||
expect(notification.fcm_push_data[:primary_actor]).to eq({
|
||||
'id' => notification.primary_actor.display_id
|
||||
})
|
||||
end
|
||||
|
||||
it 'returns correct data for primary actor message' do
|
||||
message = create(:message, sender: create(:user), content: Faker::Lorem.paragraphs(number: 2))
|
||||
notification = create(:notification, notification_type: 'assigned_conversation_new_message', primary_actor: message)
|
||||
|
||||
expect(notification.fcm_push_data[:primary_actor]).to eq({
|
||||
'id' => notification.primary_actor.id,
|
||||
'conversation_id' => notification.primary_actor.conversation.display_id
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
15
spec/presenters/html_parser_spec.rb
Normal file
15
spec/presenters/html_parser_spec.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require 'rails_helper'
|
||||
RSpec.describe HtmlParser do
|
||||
include ActionMailbox::TestHelper
|
||||
|
||||
describe 'parsed mail decorator' do
|
||||
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
|
||||
|
||||
it 'parse html content in the mail' do
|
||||
decorated_html_mail = described_class.parse_reply(html_mail.text_part.decoded)
|
||||
expect(decorated_html_mail[0..70]).to eq(
|
||||
"I'm learning English as a first language for the past 13 years, but to "
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,7 @@ RSpec.describe MailPresenter do
|
|||
|
||||
describe 'parsed mail decorator' do
|
||||
let(:mail) { create_inbound_email_from_fixture('welcome.eml').mail }
|
||||
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
|
||||
let(:decorated_mail) { described_class.new(mail) }
|
||||
|
||||
let(:mail_with_no_subject) { create_inbound_email_from_fixture('mail_with_no_subject.eml').mail }
|
||||
|
@ -56,5 +57,13 @@ RSpec.describe MailPresenter do
|
|||
it 'give email from in downcased format' do
|
||||
expect(decorated_mail.from.first.eql?(mail.from.first.downcase)).to eq true
|
||||
end
|
||||
|
||||
it 'parse html content in the mail' do
|
||||
decorated_html_mail = described_class.new(html_mail)
|
||||
expect(decorated_html_mail.subject).to eq('Fwd: How good are you in English? How did you improve your English?')
|
||||
expect(decorated_html_mail.text_content[:reply][0..70]).to eq(
|
||||
"I'm learning English as a first language for the past 13 years, but to "
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14533,11 +14533,6 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tween.js@~16.6.0:
|
||||
version "16.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tween.js/-/tween.js-16.6.0.tgz#739104c9336cc4f11ee53f9ce7cede51e6723624"
|
||||
integrity sha1-c5EEyTNsxPEe5T+c587eUeZyNiQ=
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
|
Loading…
Reference in a new issue