feat: Add preview for attachment messages (#1562)
Add preview for pending messages and attachments Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
db189e3c26
commit
3d2db95417
17 changed files with 434 additions and 250 deletions
|
@ -13,24 +13,14 @@ class MessageApi extends ApiClient {
|
|||
private: isPrivate,
|
||||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
}) {
|
||||
return axios.post(`${this.url}/${conversationId}/messages`, {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
getPreviousMessages({ conversationId, before }) {
|
||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||
params: { before },
|
||||
});
|
||||
}
|
||||
|
||||
sendAttachment([conversationId, { file, isPrivate = false }, echoId]) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachments[]', file, file.name);
|
||||
if (file) formData.append('attachments[]', file, file.name);
|
||||
if (message) formData.append('content', message);
|
||||
if (contentAttributes)
|
||||
formData.append('content_attributes', JSON.stringify(contentAttributes));
|
||||
|
||||
formData.append('private', isPrivate);
|
||||
formData.append('echo_id', echoId);
|
||||
return axios({
|
||||
|
@ -39,6 +29,12 @@ class MessageApi extends ApiClient {
|
|||
data: formData,
|
||||
});
|
||||
}
|
||||
|
||||
getPreviousMessages({ conversationId, before }) {
|
||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||
params: { before },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new MessageApi();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
@import 'shared/assets/stylesheets/spacing';
|
||||
@import 'shared/assets/stylesheets/font-size';
|
||||
@import 'shared/assets/stylesheets/font-weights';
|
||||
@import 'shared/assets/stylesheets/border-radius';
|
||||
@import 'variables';
|
||||
|
||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||
|
|
|
@ -18,13 +18,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
&::after {
|
||||
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
@ -201,6 +194,10 @@
|
|||
color: $color-body;
|
||||
margin-right: auto;
|
||||
|
||||
&.is-image {
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $color-primary-dark;
|
||||
}
|
||||
|
@ -257,6 +254,10 @@
|
|||
top: $space-smaller + $space-micro;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
border-radius: var(--border-radius-large);
|
||||
}
|
||||
}
|
||||
|
||||
+.left {
|
||||
|
@ -296,30 +297,9 @@
|
|||
border-radius: $space-smaller;
|
||||
font-size: $font-size-small;
|
||||
|
||||
p {
|
||||
color: $color-heading;
|
||||
margin-bottom: $zero;
|
||||
|
||||
.ion-person {
|
||||
color: $color-body;
|
||||
font-size: $font-size-default;
|
||||
margin-right: $space-small;
|
||||
position: relative;
|
||||
top: $space-micro;
|
||||
}
|
||||
|
||||
.message-text__wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
&::after {
|
||||
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(attachment, index) in attachments"
|
||||
:key="attachment.id"
|
||||
class="preview-item"
|
||||
>
|
||||
<div class="thumb-wrap">
|
||||
<img
|
||||
v-if="isTypeImage(attachment.resource.type)"
|
||||
class="image-thumb"
|
||||
:src="attachment.thumb"
|
||||
/>
|
||||
<span v-else class="attachment-thumb">
|
||||
📄
|
||||
</span>
|
||||
</div>
|
||||
<div class="file-name-wrap">
|
||||
<span class="item">
|
||||
{{ attachment.resource.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="file-size-wrap">
|
||||
<span class="item">
|
||||
{{ formatFileSize(attachment.resource.size) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="remove-file-wrap">
|
||||
<button
|
||||
class="remove--attachment"
|
||||
@click="() => onRemoveAttachment(index)"
|
||||
>
|
||||
<i class="ion-android-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { formatBytes } from 'dashboard/helper/files';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
removeAttachment: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onRemoveAttachment(index) {
|
||||
this.removeAttachment(index);
|
||||
},
|
||||
formatFileSize(size) {
|
||||
return formatBytes(size, 0);
|
||||
},
|
||||
isTypeImage(type) {
|
||||
return type.includes('image');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.preview-item {
|
||||
display: flex;
|
||||
padding: 0 var(--space-small) var(--space-smaller);
|
||||
}
|
||||
|
||||
.thumb-wrap {
|
||||
max-width: var(--space-jumbo);
|
||||
flex-shrink: 0;
|
||||
width: var(--space-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-thumb {
|
||||
width: var(--space-medium);
|
||||
height: var(--space-medium);
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.attachment-thumb {
|
||||
width: var(--space-medium);
|
||||
height: var(--space-medium);
|
||||
font-size: var(--font-size-medium);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name-wrap,
|
||||
.file-size-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--space-one);
|
||||
|
||||
> .item {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-mini);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: var(--space-slab) var(--space-slab) 0 var(--space-slab);
|
||||
}
|
||||
|
||||
.file-name-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-size-wrap {
|
||||
width: 20%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.remove-file-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove--attachment {
|
||||
width: var(--space-medium);
|
||||
height: var(--space-medium);
|
||||
border-radius: var(--space-medium);
|
||||
font-size: var(--font-size-small);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -14,8 +14,8 @@
|
|||
>
|
||||
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
|
||||
</span>
|
||||
<span v-if="!isPending && hasAttachments">
|
||||
<span v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image'"
|
||||
:url="attachment.data_url"
|
||||
|
@ -26,8 +26,8 @@
|
|||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<bubble-actions
|
||||
:id="data.id"
|
||||
|
@ -138,6 +138,9 @@ export default {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
hasText() {
|
||||
return !!this.data.content;
|
||||
},
|
||||
sentByMessage() {
|
||||
const { sender } = this;
|
||||
|
||||
|
@ -160,6 +163,7 @@ export default {
|
|||
bubble: this.isBubble,
|
||||
'is-private': this.data.private,
|
||||
'is-image': this.hasImageAttachment,
|
||||
'is-text': this.hasText,
|
||||
};
|
||||
},
|
||||
isPending() {
|
||||
|
@ -170,15 +174,27 @@ export default {
|
|||
</script>
|
||||
<style lang="scss">
|
||||
.wrap {
|
||||
> .is-image.bubble {
|
||||
> .bubble {
|
||||
&.is-image {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.image {
|
||||
max-width: 32rem;
|
||||
padding: 0;
|
||||
padding: var(--space-micro);
|
||||
|
||||
> img {
|
||||
border-radius: var(--border-radius-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image.is-text > .message-text__wrap {
|
||||
max-width: 32rem;
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
|
@ -188,6 +204,10 @@ export default {
|
|||
bottom: var(--space-smaller);
|
||||
right: var(--space-smaller);
|
||||
}
|
||||
|
||||
> .is-image.is-text.bubble > .message-text__wrap {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box">
|
||||
<attachment-preview
|
||||
:attachments="attachedFiles"
|
||||
:remove-attachment="removeAttachment"
|
||||
/>
|
||||
</div>
|
||||
<div class="reply-box" :class="replyBoxClass">
|
||||
<div class="reply-box__top" :class="{ 'is-private': isPrivate }">
|
||||
<canned-response
|
||||
|
@ -30,8 +37,7 @@
|
|||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<i v-if="!isUploading" class="icon ion-android-attach attachment" />
|
||||
<woot-spinner v-if="isUploading" />
|
||||
<i class="icon ion-android-attach attachment" />
|
||||
</file-upload>
|
||||
<i
|
||||
class="icon ion-happy-outline"
|
||||
|
@ -79,6 +85,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -89,6 +96,7 @@ import FileUpload from 'vue-upload-component';
|
|||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
import {
|
||||
isEscape,
|
||||
isEnter,
|
||||
|
@ -103,6 +111,7 @@ export default {
|
|||
CannedResponse,
|
||||
FileUpload,
|
||||
ResizableTextArea,
|
||||
AttachmentPreview,
|
||||
},
|
||||
mixins: [clickaway, inboxMixin],
|
||||
props: {
|
||||
|
@ -118,7 +127,7 @@ export default {
|
|||
isFocused: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedResponsesList: false,
|
||||
isUploading: false,
|
||||
attachedFiles: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -148,6 +157,8 @@ export default {
|
|||
},
|
||||
isReplyButtonDisabled() {
|
||||
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
|
||||
|
||||
if (this.hasAttachments) return false;
|
||||
return (
|
||||
isMessageEmpty ||
|
||||
this.message.length === 0 ||
|
||||
|
@ -196,9 +207,12 @@ export default {
|
|||
},
|
||||
replyBoxClass() {
|
||||
return {
|
||||
'is-focused': this.isFocused,
|
||||
'is-focused': this.isFocused || this.hasAttachments,
|
||||
};
|
||||
},
|
||||
hasAttachments() {
|
||||
return this.attachedFiles.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
|
@ -250,18 +264,11 @@ export default {
|
|||
if (this.isReplyButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
const newMessage = this.message;
|
||||
if (!this.showCannedResponsesList) {
|
||||
const newMessage = this.message;
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
this.clearMessage();
|
||||
try {
|
||||
const messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message: newMessage,
|
||||
private: this.isPrivate,
|
||||
};
|
||||
if (this.inReplyTo) {
|
||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
||||
}
|
||||
await this.$store.dispatch('sendMessage', messagePayload);
|
||||
this.$emit('scrollToMessage');
|
||||
} catch (error) {
|
||||
|
@ -288,6 +295,7 @@ export default {
|
|||
},
|
||||
clearMessage() {
|
||||
this.message = '';
|
||||
this.attachedFiles = [];
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
|
@ -322,30 +330,63 @@ export default {
|
|||
}
|
||||
},
|
||||
onFileUpload(file) {
|
||||
this.attachedFiles = [];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.isUploading = true;
|
||||
this.$store
|
||||
.dispatch('sendAttachment', [
|
||||
this.currentChat.id,
|
||||
{ file: file.file, isPrivate: this.isPrivate },
|
||||
])
|
||||
.then(() => {
|
||||
this.isUploading = false;
|
||||
this.$emit('scrollToMessage');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isUploading = false;
|
||||
this.$emit('scrollToMessage');
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file.file);
|
||||
|
||||
reader.onloadend = () => {
|
||||
this.attachedFiles.push({
|
||||
currentChatId: this.currentChat.id,
|
||||
resource: file,
|
||||
isPrivate: this.isPrivate,
|
||||
thumb: reader.result,
|
||||
});
|
||||
};
|
||||
},
|
||||
removeAttachment(itemIndex) {
|
||||
this.attachedFiles = this.attachedFiles.filter(
|
||||
(item, index) => itemIndex !== index
|
||||
);
|
||||
},
|
||||
getMessagePayload(message) {
|
||||
const [attachment] = this.attachedFiles;
|
||||
const messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message,
|
||||
private: this.isPrivate,
|
||||
};
|
||||
|
||||
if (this.inReplyTo) {
|
||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
messagePayload.file = attachment.resource.file;
|
||||
}
|
||||
|
||||
return messagePayload;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
.send-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.attachment-preview-box {
|
||||
margin: 0 var(--space-normal);
|
||||
background: var(--white);
|
||||
margin-bottom: var(--space-minus-slab);
|
||||
padding-top: var(--space-small);
|
||||
padding-bottom: var(--space-normal);
|
||||
border-top-left-radius: var(--border-radius-medium);
|
||||
border-top-right-radius: var(--border-radius-medium);
|
||||
@include shadow;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span class="message-text__wrap">
|
||||
<div class="message-text__wrap">
|
||||
<span v-html="message"></span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -33,37 +33,19 @@ export const getTypingUsersText = (users = []) => {
|
|||
export const createPendingMessage = data => {
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000);
|
||||
const tempMessageId = getUuid();
|
||||
const { message, file } = data;
|
||||
const tempAttachments = [{ id: tempMessageId }];
|
||||
const pendingMessage = {
|
||||
...data,
|
||||
content: data.message,
|
||||
content: message || null,
|
||||
id: tempMessageId,
|
||||
echo_id: tempMessageId,
|
||||
status: MESSAGE_STATUS.PROGRESS,
|
||||
created_at: timestamp,
|
||||
message_type: MESSAGE_TYPE.OUTGOING,
|
||||
conversation_id: data.conversationId,
|
||||
attachments: file ? tempAttachments : null,
|
||||
};
|
||||
return pendingMessage;
|
||||
};
|
||||
|
||||
export const createPendingAttachment = data => {
|
||||
const [conversationId, { isPrivate = false }] = data;
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000);
|
||||
const tempMessageId = getUuid();
|
||||
const pendingMessage = {
|
||||
id: tempMessageId,
|
||||
echo_id: tempMessageId,
|
||||
status: MESSAGE_STATUS.PROGRESS,
|
||||
created_at: timestamp,
|
||||
message_type: MESSAGE_TYPE.OUTGOING,
|
||||
conversation_id: conversationId,
|
||||
attachments: [
|
||||
{
|
||||
id: tempMessageId,
|
||||
},
|
||||
],
|
||||
private: isPrivate,
|
||||
content: null,
|
||||
};
|
||||
return pendingMessage;
|
||||
};
|
||||
|
|
11
app/javascript/dashboard/helper/files.js
Normal file
11
app/javascript/dashboard/helper/files.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
getTypingUsersText,
|
||||
createPendingMessage,
|
||||
createPendingAttachment,
|
||||
} from '../commons';
|
||||
import { getTypingUsersText, createPendingMessage } from '../commons';
|
||||
|
||||
describe('#getTypingUsersText', () => {
|
||||
it('returns the correct text is there is only one typing user', () => {
|
||||
|
@ -58,12 +54,13 @@ describe('#createPendingMessage', () => {
|
|||
echo_id: pending.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createPendingAttachment', () => {
|
||||
const message = [1, { isPrivate: false }];
|
||||
it('returns the pending message with expected new keys', () => {
|
||||
expect(createPendingAttachment(message)).toHaveProperty(
|
||||
it('returns the pending message with attachmnet key if file is passed', () => {
|
||||
const messageWithFile = {
|
||||
message: 'hi',
|
||||
file: {},
|
||||
};
|
||||
expect(createPendingMessage(messageWithFile)).toHaveProperty(
|
||||
'content',
|
||||
'id',
|
||||
'status',
|
||||
|
@ -77,21 +74,12 @@ describe('#createPendingAttachment', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('returns the pending message with status progress', () => {
|
||||
expect(createPendingAttachment(message)).toMatchObject({
|
||||
status: 'progress',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message with same id and echo_id', () => {
|
||||
const pending = createPendingAttachment(message);
|
||||
expect(pending).toMatchObject({
|
||||
echo_id: pending.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the pending message to have one attachment', () => {
|
||||
const pending = createPendingAttachment(message);
|
||||
const messageWithFile = {
|
||||
message: 'hi',
|
||||
file: {},
|
||||
};
|
||||
const pending = createPendingMessage(messageWithFile);
|
||||
expect(pending.attachments.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
18
app/javascript/dashboard/helper/specs/files.spec.js
Normal file
18
app/javascript/dashboard/helper/specs/files.spec.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { formatBytes } from '../files';
|
||||
|
||||
describe('#File Helpers', () => {
|
||||
describe('formatBytes', () => {
|
||||
it('should return zero bytes if 0 is passed', () => {
|
||||
expect(formatBytes(0)).toBe('0 Bytes');
|
||||
});
|
||||
it('should return in bytes if 1000 is passed', () => {
|
||||
expect(formatBytes(1000)).toBe('1000 Bytes');
|
||||
});
|
||||
it('should return in KB if 100000 is passed', () => {
|
||||
expect(formatBytes(10000)).toBe('9.77 KB');
|
||||
});
|
||||
it('should return in MB if 10000000 is passed', () => {
|
||||
expect(formatBytes(10000000)).toBe('9.54 MB');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,10 +3,7 @@ import * as types from '../../mutation-types';
|
|||
import ConversationApi from '../../../api/inbox/conversation';
|
||||
import MessageApi from '../../../api/inbox/message';
|
||||
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import {
|
||||
createPendingMessage,
|
||||
createPendingAttachment,
|
||||
} from 'dashboard/helper/commons';
|
||||
import { createPendingMessage } from 'dashboard/helper/commons';
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
|
@ -215,20 +212,6 @@ const actions = {
|
|||
commit(types.default.SET_ACTIVE_INBOX, inboxId);
|
||||
},
|
||||
|
||||
sendAttachment: async ({ commit }, data) => {
|
||||
try {
|
||||
const pendingMessage = createPendingAttachment(data);
|
||||
commit(types.default.ADD_MESSAGE, pendingMessage);
|
||||
const response = await MessageApi.sendAttachment([
|
||||
...data,
|
||||
pendingMessage.id,
|
||||
]);
|
||||
commit(types.default.ADD_MESSAGE, response.data);
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
muteConversation: async ({ commit }, conversationId) => {
|
||||
try {
|
||||
await ConversationApi.mute(conversationId);
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
:root {
|
||||
// border-radius
|
||||
--border-radius-small: 0.3rem;
|
||||
--border-radius-normal: 0.5rem;
|
||||
--border-radius-medium: 0.7rem;
|
||||
--border-radius-large: 0.9rem;
|
||||
}
|
|
@ -14,14 +14,18 @@
|
|||
</div>
|
||||
<div class="message-wrap">
|
||||
<AgentMessageBubble
|
||||
v-if="!hasAttachments && shouldDisplayAgentMessage"
|
||||
v-if="shouldDisplayAgentMessage"
|
||||
:content-type="contentType"
|
||||
:message-content-attributes="messageContentAttributes"
|
||||
:message-id="message.id"
|
||||
:message-type="messageType"
|
||||
:message="message.content"
|
||||
/>
|
||||
<div v-if="hasAttachments" class="chat-bubble has-attachment agent">
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="chat-bubble has-attachment agent"
|
||||
:class="wrapClass"
|
||||
>
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||
<file-bubble
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
|
@ -87,6 +91,7 @@ export default {
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
if (!this.message.content) return false;
|
||||
return true;
|
||||
},
|
||||
hasAttachments() {
|
||||
|
@ -167,6 +172,11 @@ export default {
|
|||
})
|
||||
);
|
||||
},
|
||||
wrapClass() {
|
||||
return {
|
||||
'has-text': this.shouldDisplayAgentMessage,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -213,6 +223,10 @@ export default {
|
|||
.has-attachment {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&.has-text {
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-message-wrap {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
v-for="message in unreadMessages"
|
||||
:key="message.id"
|
||||
:message-id="message.id"
|
||||
:message="message.content"
|
||||
:message="getMessageContent(message)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -88,6 +88,16 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
getMessageContent(message) {
|
||||
const { attachments, content } = message;
|
||||
const hasAttachments = attachments && attachments.length;
|
||||
|
||||
if (content) return content;
|
||||
|
||||
if (hasAttachments) return `📑`;
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -6,13 +6,18 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
|
|||
end
|
||||
|
||||
def perform_reply
|
||||
result = FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
|
||||
message.update!(source_id: JSON.parse(result)['message_id'])
|
||||
send_message_to_facebook fb_text_message_params if message.content.present?
|
||||
send_message_to_facebook fb_attachment_message_params if message.attachments.present?
|
||||
rescue Facebook::Messenger::FacebookError => e
|
||||
Rails.logger.info e
|
||||
channel.authorization_error!
|
||||
end
|
||||
|
||||
def send_message_to_facebook(delivery_params)
|
||||
result = FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
|
||||
message.update!(source_id: JSON.parse(result)['message_id'])
|
||||
end
|
||||
|
||||
def fb_text_message_params
|
||||
{
|
||||
recipient: { id: contact.get_source_id(inbox.id) },
|
||||
|
@ -49,29 +54,6 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
|
|||
end
|
||||
end
|
||||
|
||||
def delivery_params
|
||||
if twenty_four_hour_window_over?
|
||||
fb_message_params.merge(tag: 'ISSUE_RESOLUTION')
|
||||
else
|
||||
fb_message_params
|
||||
end
|
||||
end
|
||||
|
||||
def twenty_four_hour_window_over?
|
||||
return false unless after_24_hours?
|
||||
return false if last_incoming_and_outgoing_message_after_one_day?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def last_incoming_and_outgoing_message_after_one_day?
|
||||
last_incoming_message && sent_first_outgoing_message_after_24_hours?
|
||||
end
|
||||
|
||||
def after_24_hours?
|
||||
(Time.current - last_incoming_message.created_at) / 3600 >= 24
|
||||
end
|
||||
|
||||
def sent_first_outgoing_message_after_24_hours?
|
||||
# we can send max 1 message after 24 hour window
|
||||
conversation.messages.outgoing.where('id > ?', last_incoming_message.id).count == 1
|
||||
|
|
|
@ -58,7 +58,21 @@ describe Facebook::SendOnFacebookService do
|
|||
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||
message.save!
|
||||
::Facebook::SendOnFacebookService.new(message: message).perform
|
||||
expect(bot).to have_received(:deliver)
|
||||
expect(bot).to have_received(:deliver).with({
|
||||
recipient: { id: contact_inbox.source_id },
|
||||
message: { text: message.content }
|
||||
}, { access_token: facebook_channel.page_access_token })
|
||||
expect(bot).to have_received(:deliver).with({
|
||||
recipient: { id: contact_inbox.source_id },
|
||||
message: {
|
||||
attachment: {
|
||||
type: 'image',
|
||||
payload: {
|
||||
url: attachment.file_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { access_token: facebook_channel.page_access_token })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue