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:
Nithin David Thomas
2020-04-02 12:28:38 +05:30
committed by GitHub
parent 0afa5c297f
commit 7fcd2d0e85
28 changed files with 338 additions and 69 deletions

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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`,

View File

@@ -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&amp;X-Amz-Credential=AKIAI3KBM2ES3VRHQHPQ%2F20170422%2Fus-west-2%2Fs3%2Faws4_request&amp;X-Amz-Date=20170422T075421Z&amp;X-Amz-Expires=604800&amp;X-Amz-SignedHeaders=host&amp;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: {

View File

@@ -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');

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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({

View File

@@ -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>`
); );
} }

View File

@@ -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>'
); );
}); });
}); });

View File

@@ -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;

View File

@@ -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,
}); });

View File

@@ -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()) {

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
export default {
COMPONENTS: {
FILE_BUBBLE: {
DOWNLOAD: 'Download',
UPLOADING: 'Uploading...',
},
},
};

View File

@@ -0,0 +1,5 @@
import en from './en';
export default {
en,
};

View File

@@ -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,
});
} }
}, },

View File

@@ -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',
}, },
}); });

View File

@@ -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',
},
},
}); });
}); });
}); });

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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