mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 04:27:53 +00:00
Feature: Support file type messages on widget and dashboard (#659)
- Adds support for file upload Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com> Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
committed by
GitHub
parent
0afa5c297f
commit
7fcd2d0e85
@@ -1,4 +1,5 @@
|
|||||||
class Messages::Outgoing::NormalBuilder
|
class Messages::Outgoing::NormalBuilder
|
||||||
|
include ::FileTypeHelper
|
||||||
attr_reader :message
|
attr_reader :message
|
||||||
|
|
||||||
def initialize(user, conversation, params)
|
def initialize(user, conversation, params)
|
||||||
@@ -13,7 +14,10 @@ class Messages::Outgoing::NormalBuilder
|
|||||||
def perform
|
def perform
|
||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
if @attachment
|
if @attachment
|
||||||
@message.attachment = Attachment.new(account_id: message.account_id)
|
@message.attachment = Attachment.new(
|
||||||
|
account_id: message.account_id,
|
||||||
|
file_type: file_type(@attachment[:file]&.content_type)
|
||||||
|
)
|
||||||
@message.attachment.file.attach(@attachment[:file])
|
@message.attachment.file.attach(@attachment[:file])
|
||||||
end
|
end
|
||||||
@message.save
|
@message.save
|
||||||
|
|||||||
@@ -10,12 +10,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
@message = conversation.messages.new(message_params)
|
@message = conversation.messages.new(message_params)
|
||||||
|
build_attachment
|
||||||
@message.save!
|
@message.save!
|
||||||
if params[:message][:attachment].present?
|
|
||||||
@message.attachment = Attachment.new(account_id: @message.account_id)
|
|
||||||
@message.attachment.file.attach(params[:message][:attachment][:file])
|
|
||||||
end
|
|
||||||
render json: @message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -28,6 +24,16 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def build_attachment
|
||||||
|
return if params[:message][:attachment].blank?
|
||||||
|
|
||||||
|
@message.attachment = Attachment.new(
|
||||||
|
account_id: @message.account_id,
|
||||||
|
file_type: helpers.file_type(params[:message][:attachment][:file]&.content_type)
|
||||||
|
)
|
||||||
|
@message.attachment.file.attach(params[:message][:attachment][:file])
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation
|
def set_conversation
|
||||||
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
||||||
end
|
end
|
||||||
|
|||||||
14
app/helpers/file_type_helper.rb
Normal file
14
app/helpers/file_type_helper.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module FileTypeHelper
|
||||||
|
def file_type(content_type)
|
||||||
|
return :image if [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/gif',
|
||||||
|
'image/tiff',
|
||||||
|
'image/bmp'
|
||||||
|
].include?(content_type)
|
||||||
|
|
||||||
|
:file
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,10 +20,9 @@ class MessageApi extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAttachment([conversationId, { file, file_type }]) {
|
sendAttachment([conversationId, { file }]) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('attachment[file]', file);
|
formData.append('attachment[file]', file);
|
||||||
formData.append('attachment[file_type]', file_type);
|
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: `${this.url}/${conversationId}/messages`,
|
url: `${this.url}/${conversationId}/messages`,
|
||||||
|
|||||||
@@ -7,18 +7,11 @@
|
|||||||
:url="data.attachment.data_url"
|
:url="data.attachment.data_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
/>
|
/>
|
||||||
<bubble-audio
|
<bubble-file
|
||||||
v-if="data.attachment && data.attachment.file_type === 'audio'"
|
v-if="data.attachment && data.attachment.file_type !== 'image'"
|
||||||
:url="data.attachment.data_url"
|
:url="data.attachment.data_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
/>
|
/>
|
||||||
<bubble-map
|
|
||||||
v-if="data.attachment && data.attachment.file_type === 'location'"
|
|
||||||
:lat="data.attachment.coordinates_lat"
|
|
||||||
:lng="data.attachment.coordinates_long"
|
|
||||||
:label="data.attachment.fallback_title"
|
|
||||||
:readable-time="readableTime"
|
|
||||||
/>
|
|
||||||
<bubble-text
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
@@ -33,25 +26,25 @@
|
|||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- <img v-if="showSenderData" src="https://chatwoot-staging.s3-us-west-2.amazonaws.com/uploads/avatar/contact/3415/thumb_10418362_10201264050880840_6087258728802054624_n.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAI3KBM2ES3VRHQHPQ%2F20170422%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20170422T075421Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=8d5ff60e41415515f59ff682b9a4e4c0574d9d9aabfeff1dc5a51087a9b49e03" class="sender--thumbnail"> -->
|
<!-- <img
|
||||||
|
src="https://randomuser.me/api/portraits/women/94.jpg"
|
||||||
|
class="sender--thumbnail"
|
||||||
|
/> -->
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
/* eslint-disable no-named-as-default */
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import getEmojiSVG from '../emoji/utils';
|
import getEmojiSVG from '../emoji/utils';
|
||||||
import timeMixin from '../../../mixins/time';
|
import timeMixin from '../../../mixins/time';
|
||||||
import BubbleText from './bubble/Text';
|
import BubbleText from './bubble/Text';
|
||||||
import BubbleImage from './bubble/Image';
|
import BubbleImage from './bubble/Image';
|
||||||
import BubbleMap from './bubble/Map';
|
import BubbleFile from './bubble/File';
|
||||||
import BubbleAudio from './bubble/Audio';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BubbleText,
|
BubbleText,
|
||||||
BubbleImage,
|
BubbleImage,
|
||||||
BubbleMap,
|
BubbleFile,
|
||||||
BubbleAudio,
|
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, messageFormatterMixin],
|
mixins: [timeMixin, messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
@blur="onBlur()"
|
@blur="onBlur()"
|
||||||
/>
|
/>
|
||||||
<file-upload
|
<file-upload
|
||||||
v-if="!showFileUpload"
|
v-if="showFileUpload"
|
||||||
accept="image/*"
|
:size="4096 * 4096"
|
||||||
@input-file="onFileUpload"
|
@input-file="onFileUpload"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
@@ -105,7 +105,6 @@ export default {
|
|||||||
message: '',
|
message: '',
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
showEmojiPicker: false,
|
showEmojiPicker: false,
|
||||||
showFileUpload: false,
|
|
||||||
showCannedResponsesList: false,
|
showCannedResponsesList: false,
|
||||||
isUploading: {
|
isUploading: {
|
||||||
audio: false,
|
audio: false,
|
||||||
@@ -142,6 +141,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return 10000;
|
return 10000;
|
||||||
},
|
},
|
||||||
|
showFileUpload() {
|
||||||
|
return this.channelType === 'Channel::WebWidget';
|
||||||
|
},
|
||||||
replyButtonLabel() {
|
replyButtonLabel() {
|
||||||
if (this.isPrivate) {
|
if (this.isPrivate) {
|
||||||
return this.$t('CONVERSATION.REPLYBOX.CREATE');
|
return this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||||
@@ -295,13 +297,7 @@ export default {
|
|||||||
onFileUpload(file) {
|
onFileUpload(file) {
|
||||||
this.isUploading.image = true;
|
this.isUploading.image = true;
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('sendAttachment', [
|
.dispatch('sendAttachment', [this.currentChat.id, { file: file.file }])
|
||||||
this.currentChat.id,
|
|
||||||
{
|
|
||||||
file_type: file.type,
|
|
||||||
file: file.file,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isUploading.image = false;
|
this.isUploading.image = false;
|
||||||
this.$emit('scrollToMessage');
|
this.$emit('scrollToMessage');
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file message-text__wrap" @click="openLink">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<i class="ion-document-text"></i>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<h5 class="text-block-title">
|
||||||
|
{{ decodeURI(fileName) }}
|
||||||
|
</h5>
|
||||||
|
<a
|
||||||
|
class="download clear button small"
|
||||||
|
rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
:href="url"
|
||||||
|
>
|
||||||
|
{{ $t('CONVERSATION.DOWNLOAD') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<span class="time">{{ readableTime }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ['url', 'readableTime'],
|
||||||
|
computed: {
|
||||||
|
fileName() {
|
||||||
|
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||||
|
return filename;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openLink() {
|
||||||
|
const win = window.open(this.url, '_blank');
|
||||||
|
win.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: $space-normal;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
font-size: $font-size-giga;
|
||||||
|
color: $color-woot;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: $space-smaller;
|
||||||
|
margin-right: $space-slab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block-title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
padding-right: $space-two;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,6 +11,10 @@ export default {
|
|||||||
BUTTON_TEXT: 'Copy',
|
BUTTON_TEXT: 'Copy',
|
||||||
COPY_SUCCESSFUL: 'Code copied to clipboard successfully',
|
COPY_SUCCESSFUL: 'Code copied to clipboard successfully',
|
||||||
},
|
},
|
||||||
|
FILE_BUBBLE: {
|
||||||
|
DOWNLOAD: 'Download',
|
||||||
|
UPLOADING: 'Uploading...',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
CONFIRM_EMAIL: 'Verifying...',
|
CONFIRM_EMAIL: 'Verifying...',
|
||||||
SETTINGS: {
|
SETTINGS: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"CLICK_HERE": "Click here",
|
"CLICK_HERE": "Click here",
|
||||||
"LOADING_INBOXES": "Loading inboxes",
|
"LOADING_INBOXES": "Loading inboxes",
|
||||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||||
|
"DOWNLOAD": "Download",
|
||||||
"HEADER": {
|
"HEADER": {
|
||||||
"RESOLVE_ACTION": "Resolve",
|
"RESOLVE_ACTION": "Resolve",
|
||||||
"REOPEN_ACTION": "Reopen",
|
"REOPEN_ACTION": "Reopen",
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuelidate from 'vuelidate';
|
import Vuelidate from 'vuelidate';
|
||||||
|
import VueI18n from 'vue-i18n';
|
||||||
import store from '../widget/store';
|
import store from '../widget/store';
|
||||||
import App from '../widget/App.vue';
|
import App from '../widget/App.vue';
|
||||||
import router from '../widget/router';
|
import router from '../widget/router';
|
||||||
import ActionCableConnector from '../widget/helpers/actionCable';
|
import ActionCableConnector from '../widget/helpers/actionCable';
|
||||||
|
import i18n from '../widget/i18n';
|
||||||
|
|
||||||
|
Vue.use(VueI18n);
|
||||||
Vue.use(Vuelidate);
|
Vue.use(Vuelidate);
|
||||||
|
|
||||||
|
Vue.config.lang = 'en';
|
||||||
|
Object.keys(i18n).forEach(lang => {
|
||||||
|
Vue.locale(lang, i18n[lang]);
|
||||||
|
});
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
window.WOOT_WIDGET = new Vue({
|
window.WOOT_WIDGET = new Vue({
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class MessageFormatter {
|
|||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
return this.message.replace(
|
return this.message.replace(
|
||||||
urlRegex,
|
urlRegex,
|
||||||
url => `<a href="${url}" target="_blank">${url}</a>`
|
url =>
|
||||||
|
`<a rel="noreferrer noopener nofollow" href="${url}" target="_blank">${url}</a>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ describe('#MessageFormatter', () => {
|
|||||||
const message =
|
const message =
|
||||||
'Chatwoot is an opensource tool\nSee more at https://www.chatwoot.com';
|
'Chatwoot is an opensource tool\nSee more at https://www.chatwoot.com';
|
||||||
expect(new MessageFormatter(message).formattedMessage).toEqual(
|
expect(new MessageFormatter(message).formattedMessage).toEqual(
|
||||||
'Chatwoot is an opensource tool<br>See more at <a href="https://www.chatwoot.com" target="_blank">https://www.chatwoot.com</a>'
|
'Chatwoot is an opensource tool<br>See more at <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" target="_blank">https://www.chatwoot.com</a>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,8 +17,13 @@
|
|||||||
:message-type="messageType"
|
:message-type="messageType"
|
||||||
:message="message.content"
|
:message="message.content"
|
||||||
/>
|
/>
|
||||||
<div v-if="hasImage" class="chat-bubble has-attachment agent">
|
<div v-if="hasAttachment" class="chat-bubble has-attachment agent">
|
||||||
|
<file-bubble
|
||||||
|
v-if="message.attachment && message.attachment.file_type !== 'image'"
|
||||||
|
:url="message.attachment.data_url"
|
||||||
|
/>
|
||||||
<image-bubble
|
<image-bubble
|
||||||
|
v-else
|
||||||
:url="message.attachment.data_url"
|
:url="message.attachment.data_url"
|
||||||
:thumb="message.attachment.thumb_url"
|
:thumb="message.attachment.thumb_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
@@ -35,6 +40,7 @@
|
|||||||
import AgentMessageBubble from 'widget/components/AgentMessageBubble';
|
import AgentMessageBubble from 'widget/components/AgentMessageBubble';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
import ImageBubble from 'widget/components/ImageBubble';
|
import ImageBubble from 'widget/components/ImageBubble';
|
||||||
|
import FileBubble from 'widget/components/FileBubble';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
|
|
||||||
@@ -44,6 +50,7 @@ export default {
|
|||||||
AgentMessageBubble,
|
AgentMessageBubble,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
ImageBubble,
|
ImageBubble,
|
||||||
|
FileBubble,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
mixins: [timeMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -53,10 +60,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasImage() {
|
hasAttachment() {
|
||||||
const { attachment = {} } = this.message;
|
return !!this.message.attachment;
|
||||||
const { file_type: fileType } = attachment;
|
|
||||||
return fileType === 'image';
|
|
||||||
},
|
},
|
||||||
showTextBubble() {
|
showTextBubble() {
|
||||||
const { message } = this;
|
const { message } = this;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<file-upload accept="image/*" @input-file="onFileUpload">
|
<file-upload :size="4096 * 2048" @input-file="onFileUpload">
|
||||||
<span class="attachment-button ">
|
<span class="attachment-button ">
|
||||||
<i v-if="!isUploading.image"></i>
|
<i v-if="!isUploading.image"></i>
|
||||||
<spinner v-if="isUploading" size="small" />
|
<spinner v-if="isUploading" size="small" />
|
||||||
@@ -23,12 +23,15 @@ export default {
|
|||||||
return { isUploading: false };
|
return { isUploading: false };
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getFileType(fileType) {
|
||||||
|
return fileType.includes('image') ? 'image' : 'file';
|
||||||
|
},
|
||||||
async onFileUpload(file) {
|
async onFileUpload(file) {
|
||||||
this.isUploading = true;
|
this.isUploading = true;
|
||||||
try {
|
try {
|
||||||
const thumbUrl = window.URL.createObjectURL(file.file);
|
const thumbUrl = window.URL.createObjectURL(file.file);
|
||||||
await this.onAttach({
|
await this.onAttach({
|
||||||
file_type: file.type,
|
fileType: this.getFileType(file.type),
|
||||||
file: file.file,
|
file: file.file,
|
||||||
thumbUrl,
|
thumbUrl,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-message--input">
|
<div class="chat-message--input">
|
||||||
<chat-attchment-button :on-attach="onSendAttachment" />
|
<chat-attachment-button :on-attach="onSendAttachment" />
|
||||||
<ChatInputArea v-model="userInput" :placeholder="placeholder" />
|
<ChatInputArea v-model="userInput" :placeholder="placeholder" />
|
||||||
<ChatSendButton
|
<ChatSendButton
|
||||||
:on-click="handleButtonClick"
|
:on-click="handleButtonClick"
|
||||||
@@ -13,13 +13,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||||
import ChatAttchmentButton from 'widget/components/ChatAttachment.vue';
|
import ChatAttachmentButton from 'widget/components/ChatAttachment.vue';
|
||||||
import ChatInputArea from 'widget/components/ChatInputArea.vue';
|
import ChatInputArea from 'widget/components/ChatInputArea.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatInputWrap',
|
name: 'ChatInputWrap',
|
||||||
components: {
|
components: {
|
||||||
ChatAttchmentButton,
|
ChatAttachmentButton,
|
||||||
ChatSendButton,
|
ChatSendButton,
|
||||||
ChatInputArea,
|
ChatInputArea,
|
||||||
},
|
},
|
||||||
@@ -44,6 +44,13 @@ export default {
|
|||||||
userInput: '',
|
userInput: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
destroyed() {
|
destroyed() {
|
||||||
document.removeEventListener('keypress', this.handleEnterKeyPress);
|
document.removeEventListener('keypress', this.handleEnterKeyPress);
|
||||||
},
|
},
|
||||||
@@ -51,11 +58,6 @@ export default {
|
|||||||
document.addEventListener('keypress', this.handleEnterKeyPress);
|
document.addEventListener('keypress', this.handleEnterKeyPress);
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
widgetColor: 'appConfig/getWidgetColor',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
handleButtonClick() {
|
handleButtonClick() {
|
||||||
if (this.userInput && this.userInput.trim()) {
|
if (this.userInput && this.userInput.trim()) {
|
||||||
|
|||||||
93
app/javascript/widget/components/FileBubble.vue
Normal file
93
app/javascript/widget/components/FileBubble.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file message-text__wrap" @click="openLink">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<i class="ion-document-text"></i>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<div class="title">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div class="link-wrap">
|
||||||
|
<a
|
||||||
|
class="download"
|
||||||
|
rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
:href="url"
|
||||||
|
>
|
||||||
|
{{ $t('COMPONENTS.FILE_BUBBLE.DOWNLOAD') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isInProgress: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.isInProgress
|
||||||
|
? this.$t('COMPONENTS.FILE_BUBBLE.UPLOADING')
|
||||||
|
: decodeURI(this.fileName);
|
||||||
|
},
|
||||||
|
fileName() {
|
||||||
|
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||||
|
return filename;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openLink() {
|
||||||
|
const win = window.open(this.url, '_blank');
|
||||||
|
win.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~widget/assets/scss/variables.scss';
|
||||||
|
|
||||||
|
.file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: $space-one $space-slab;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
font-size: $font-size-bigger;
|
||||||
|
color: $color-woot;
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: $space-smaller;
|
||||||
|
margin-right: $space-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-wrap {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
padding-right: $space-smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<a :href="url" target="_blank" class="image">
|
<a
|
||||||
|
:href="url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener nofollow"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<img :src="thumb" alt="Picture message" />
|
<img :src="thumb" alt="Picture message" />
|
||||||
<span class="time">{{ readableTime }}</span>
|
<span class="time">{{ readableTime }}</span>
|
||||||
|
|||||||
@@ -6,8 +6,14 @@
|
|||||||
:message="message.content"
|
:message="message.content"
|
||||||
:status="message.status"
|
:status="message.status"
|
||||||
/>
|
/>
|
||||||
<div v-if="hasImage" class="chat-bubble has-attachment user">
|
<div v-if="hasAttachment" class="chat-bubble has-attachment user">
|
||||||
|
<file-bubble
|
||||||
|
v-if="message.attachment && message.attachment.file_type !== 'image'"
|
||||||
|
:url="message.attachment.data_url"
|
||||||
|
:is-in-progress="isInProgress"
|
||||||
|
/>
|
||||||
<image-bubble
|
<image-bubble
|
||||||
|
v-else
|
||||||
:url="message.attachment.data_url"
|
:url="message.attachment.data_url"
|
||||||
:thumb="message.attachment.thumb_url"
|
:thumb="message.attachment.thumb_url"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
@@ -20,6 +26,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import UserMessageBubble from 'widget/components/UserMessageBubble';
|
import UserMessageBubble from 'widget/components/UserMessageBubble';
|
||||||
import ImageBubble from 'widget/components/ImageBubble';
|
import ImageBubble from 'widget/components/ImageBubble';
|
||||||
|
import FileBubble from 'widget/components/FileBubble';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -27,6 +34,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
UserMessageBubble,
|
UserMessageBubble,
|
||||||
ImageBubble,
|
ImageBubble,
|
||||||
|
FileBubble,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
mixins: [timeMixin],
|
||||||
props: {
|
props: {
|
||||||
@@ -40,11 +48,8 @@ export default {
|
|||||||
const { status = '' } = this.message;
|
const { status = '' } = this.message;
|
||||||
return status === 'in_progress';
|
return status === 'in_progress';
|
||||||
},
|
},
|
||||||
hasImage() {
|
hasAttachment() {
|
||||||
const { attachment = {} } = this.message;
|
return !!this.message.attachment;
|
||||||
const { file_type: fileType } = attachment;
|
|
||||||
|
|
||||||
return fileType === 'image';
|
|
||||||
},
|
},
|
||||||
showTextBubble() {
|
showTextBubble() {
|
||||||
const { message } = this;
|
const { message } = this;
|
||||||
@@ -94,5 +99,14 @@ export default {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.user.has-attachment {
|
||||||
|
.icon-wrap {
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export default {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
max-width: 80%;
|
|
||||||
padding: $space-small $space-normal;
|
padding: $space-small $space-normal;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
|||||||
8
app/javascript/widget/i18n/en.js
Normal file
8
app/javascript/widget/i18n/en.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
COMPONENTS: {
|
||||||
|
FILE_BUBBLE: {
|
||||||
|
DOWNLOAD: 'Download',
|
||||||
|
UPLOADING: 'Uploading...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
5
app/javascript/widget/i18n/index.js
Normal file
5
app/javascript/widget/i18n/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import en from './en';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
en,
|
||||||
|
};
|
||||||
@@ -88,15 +88,16 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sendAttachment: async ({ commit }, params) => {
|
sendAttachment: async ({ commit }, params) => {
|
||||||
const { attachment } = params;
|
const {
|
||||||
const { thumbUrl } = attachment;
|
attachment: { thumbUrl, fileType },
|
||||||
const attachmentBlob = {
|
} = params;
|
||||||
|
const attachment = {
|
||||||
thumb_url: thumbUrl,
|
thumb_url: thumbUrl,
|
||||||
data_url: thumbUrl,
|
data_url: thumbUrl,
|
||||||
file_type: 'image',
|
file_type: fileType,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
};
|
};
|
||||||
const tempMessage = createTemporaryMessage({ attachment: attachmentBlob });
|
const tempMessage = createTemporaryMessage({ attachment });
|
||||||
commit('pushMessageToConversation', tempMessage);
|
commit('pushMessageToConversation', tempMessage);
|
||||||
try {
|
try {
|
||||||
const { data } = await sendAttachmentAPI(params);
|
const { data } = await sendAttachmentAPI(params);
|
||||||
@@ -158,8 +159,16 @@ export const mutations = {
|
|||||||
|
|
||||||
if (messageInConversation) {
|
if (messageInConversation) {
|
||||||
Vue.delete(messagesInbox, tempId);
|
Vue.delete(messagesInbox, tempId);
|
||||||
const newMessage = { ...messageInConversation };
|
const { attachment } = messageInConversation;
|
||||||
Vue.set(messagesInbox, id, { ...newMessage, id, status });
|
if (attachment.file_type === 'file') {
|
||||||
|
attachment.data_url = message.attachment.data_url;
|
||||||
|
}
|
||||||
|
Vue.set(messagesInbox, id, {
|
||||||
|
...messageInConversation,
|
||||||
|
attachment,
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('#actions', () => {
|
|||||||
getUuid.mockImplementationOnce(() => '1111');
|
getUuid.mockImplementationOnce(() => '1111');
|
||||||
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
|
||||||
const thumbUrl = '';
|
const thumbUrl = '';
|
||||||
const attachment = { thumbUrl };
|
const attachment = { thumbUrl, fileType: 'file' };
|
||||||
|
|
||||||
actions.sendAttachment({ commit }, { attachment });
|
actions.sendAttachment({ commit }, { attachment });
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
@@ -62,7 +62,7 @@ describe('#actions', () => {
|
|||||||
attachment: {
|
attachment: {
|
||||||
thumb_url: '',
|
thumb_url: '',
|
||||||
data_url: '',
|
data_url: '',
|
||||||
file_type: 'image',
|
file_type: 'file',
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ describe('#mutations', () => {
|
|||||||
id: 'rand_id_123',
|
id: 'rand_id_123',
|
||||||
message_type: 0,
|
message_type: 0,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
|
attachment: {
|
||||||
|
file: '',
|
||||||
|
file_type: 'image',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -109,11 +113,24 @@ describe('#mutations', () => {
|
|||||||
id: '1',
|
id: '1',
|
||||||
content: '',
|
content: '',
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
|
attachment: {
|
||||||
|
file: '',
|
||||||
|
file_type: 'image',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mutations.setMessageStatus(state, { message, tempId: 'rand_id_123' });
|
mutations.setMessageStatus(state, { message, tempId: 'rand_id_123' });
|
||||||
|
|
||||||
expect(state.conversations).toEqual({
|
expect(state.conversations).toEqual({
|
||||||
1: { id: '1', content: '', message_type: 0, status: 'sent' },
|
1: {
|
||||||
|
id: '1',
|
||||||
|
content: '',
|
||||||
|
message_type: 0,
|
||||||
|
status: 'sent',
|
||||||
|
attachment: {
|
||||||
|
file: '',
|
||||||
|
file_type: 'image',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
10
app/views/api/v1/widget/messages/create.json.jbuilder
Normal file
10
app/views/api/v1/widget/messages/create.json.jbuilder
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
json.id @message.id
|
||||||
|
json.content @message.content
|
||||||
|
json.inbox_id @message.inbox_id
|
||||||
|
json.conversation_id @message.conversation.display_id
|
||||||
|
json.message_type @message.message_type_before_type_cast
|
||||||
|
json.created_at @message.created_at.to_i
|
||||||
|
json.private @message.private
|
||||||
|
json.source_id @message.source_id
|
||||||
|
json.attachment @message.attachment.push_event_data if @message.attachment
|
||||||
|
json.sender @message.user.push_event_data if @message.user
|
||||||
@@ -5,7 +5,7 @@ json.array! @messages do |message|
|
|||||||
json.content_type message.content_type
|
json.content_type message.content_type
|
||||||
json.content_attributes message.content_attributes
|
json.content_attributes message.content_attributes
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.conversation_id message. conversation_id
|
json.conversation_id message.conversation.display_id
|
||||||
json.attachment message.attachment.push_event_data if message.attachment
|
json.attachment message.attachment.push_event_data if message.attachment
|
||||||
json.sender message.user.push_event_data if message.user
|
json.sender message.user.push_event_data if message.user
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
|||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
||||||
|
expect(conversation.messages.last.attachment.file_type).to eq('image')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
|||||||
expect(json_response['content']).to eq(message_params[:content])
|
expect(json_response['content']).to eq(message_params[:content])
|
||||||
|
|
||||||
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
expect(conversation.messages.last.attachment.file.present?).to eq(true)
|
||||||
|
expect(conversation.messages.last.attachment.file_type).to eq('image')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user