Compare commits

...
Author SHA1 Message Date
Shivam Mishra 52f139561e feat: hide attachment icon 2023-10-10 19:22:11 +05:30
Shivam Mishra f509a734f9 refactor: use message preview in reply-to-message 2023-10-10 19:22:11 +05:30
Shivam Mishra e61f5a4e62 refactor: styles for message preview 2023-10-10 19:21:46 +05:30
Shivam Mishra 33cdd2c0ed refactor: split message-preview out as a component 2023-10-10 19:21:45 +05:30
Shivam Mishra a607bf4b60 refactor: visually balance y-padding 2023-10-10 19:21:45 +05:30
Shivam Mishra 7added2d48 feat: support attachments in bubble 2023-10-10 19:21:45 +05:30
Shivam Mishra d3dd20e4b9 fix: prop value 2023-10-10 19:21:45 +05:30
Shivam Mishra 8db377d29b feat: allow translation 2023-10-10 19:21:39 +05:30
Shivam Mishra 603c45a907 refactor: use vue 2 syntax 2023-10-10 19:21:38 +05:30
Shivam Mishra 1dcaec1186 feat: handle attachments in ReplyToMessage 2023-10-10 19:20:29 +05:30
Shivam Mishra d26959f57c feat: include attachments in props 2023-10-10 19:19:01 +05:30
Shivam Mishra c114cc7fa0 refactor: move ATTACHMENT_ICONS to constants 2023-10-10 19:18:34 +05:30
Shivam Mishra 7abe5b01dc fix: condition 2023-10-10 19:18:34 +05:30
Shivam Mishra 047f1f6bc3 refactor: handle translation externally 2023-10-10 19:18:34 +05:30
Shivam Mishra 86ea2bbbe3 refactor: handle missing message content 2023-10-10 19:18:34 +05:30
Shivam Mishra 53ac68f0c0 refactor: disable reply to bubble 2023-10-10 19:18:33 +05:30
Shivam Mishra a2af5e25e1 feat: allow showing in-reply-to message 2023-10-10 19:18:33 +05:30
9 changed files with 242 additions and 118 deletions
@@ -1,6 +1,6 @@
<template>
<div
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
:class="{
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
'unread-chat': hasUnread,
@@ -31,7 +31,7 @@
size="40px"
/>
<div
class="py-3 px-0 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
class="px-0 py-3 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
>
<div class="flex justify-between">
<inbox-name v-if="showInboxName" :inbox="inbox" />
@@ -55,44 +55,11 @@
>
{{ currentContact.name }}
</h4>
<p
<message-preview
v-if="lastMessageInChat"
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
>
<fluent-icon
v-if="isMessagePrivate"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="lock-closed"
/>
<fluent-icon
v-else-if="messageByAgent"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="arrow-reply"
/>
<fluent-icon
v-else-if="isMessageAnActivity"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="info"
/>
<span v-if="lastMessageInChat.content">
{{ parsedLastMessage }}
</span>
<span v-else-if="lastMessageInChat.attachments">
<fluent-icon
v-if="attachmentIcon"
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ $t(`${attachmentMessageContent}`) }}
</span>
<span v-else>
{{ $t('CHAT_LIST.NO_CONTENT') }}
</span>
</p>
:message="lastMessageInChat"
class="conversation--message mt-0 mb-2 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm"
/>
<p
v-else
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
@@ -106,8 +73,8 @@
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
</span>
</p>
<div class="conversation--meta flex flex-col absolute right-4 top-4">
<span class="text-black-600 text-xxs font-normal leading-4 ml-auto">
<div class="absolute flex flex-col conversation--meta right-4 top-4">
<span class="ml-auto font-normal leading-4 text-black-600 text-xxs">
<time-ago
:last-activity-timestamp="chat.timestamp"
:created-at-timestamp="chat.created_at"
@@ -145,9 +112,8 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
@@ -159,14 +125,6 @@ import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
const ATTACHMENT_ICONS = {
image: 'image',
audio: 'headphones-sound-wave',
video: 'video',
file: 'document',
location: 'location',
fallback: 'link',
};
export default {
components: {
@@ -175,16 +133,11 @@ export default {
Thumbnail,
ConversationContextMenu,
TimeAgo,
MessagePreview,
PriorityMark,
},
mixins: [
inboxMixin,
timeMixin,
conversationMixin,
messageFormatterMixin,
alertMixin,
],
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
props: {
activeLabel: {
type: String,
@@ -258,20 +211,6 @@ export default {
);
},
lastMessageFileType() {
const lastMessage = this.lastMessageInChat;
const [{ file_type: fileType } = {}] = lastMessage.attachments;
return fileType;
},
attachmentIcon() {
return ATTACHMENT_ICONS[this.lastMessageFileType];
},
attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
},
isActiveChat() {
return this.currentChat.id === this.chat.id;
},
@@ -292,30 +231,6 @@ export default {
return this.lastMessage(this.chat);
},
messageByAgent() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.OUTGOING;
},
isMessageAnActivity() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.ACTIVITY;
},
isMessagePrivate() {
const lastMessage = this.lastMessageInChat;
const { private: isPrivate } = lastMessage;
return isPrivate;
},
parsedLastMessage() {
const { content_attributes: contentAttributes } = this.lastMessageInChat;
const { email: { subject } = {} } = contentAttributes || {};
return this.getPlainText(subject || this.lastMessageInChat.content);
},
inbox() {
const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
@@ -34,6 +34,13 @@
:url="storyUrl"
/>
</blockquote>
<bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo"
:content="inReplyTo.content"
:attachments="inReplyTo.attachments"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
@click="navigateToMessage"
/>
<bubble-text
v-if="data.content"
:message="message"
@@ -141,6 +148,7 @@ import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleText from './bubble/Text.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
@@ -165,6 +173,7 @@ export default {
BubbleMailHead,
BubbleText,
BubbleContact,
BubbleReplyTo,
ContextMenu,
Spinner,
instagramImageErrorPlaceholder,
@@ -175,6 +184,10 @@ export default {
type: Object,
required: true,
},
currentChat: {
type: Object,
required: true,
},
isATweet: {
type: Boolean,
default: false,
@@ -271,6 +284,21 @@ export default {
) + botMessageContent
);
},
inReplyToMessageId() {
return this.data.content_attributes?.in_reply_to;
},
inReplyTo() {
if (!this.inReplyToMessageId) return null;
const replyTo = this.currentChat?.messages.find(message => {
if (message.id === this.inReplyToMessageId) {
return true;
}
return false;
});
return replyTo ?? {};
},
contextMenuEnabledOptions() {
return {
copy: this.hasText,
@@ -522,6 +550,13 @@ export default {
this.showBackgroundHighlight = false;
}, HIGHLIGHT_TIMER);
},
navigateToMessage() {
this.$nextTick(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
messageId: this.inReplyToMessageId,
});
});
},
},
};
</script>
@@ -0,0 +1,91 @@
<template>
<span
class="overflow-hidden text-slate-700 dark:text-slate-200 text-ellipsis whitespace-nowrap"
>
<template v-if="showMessageType">
<fluent-icon
v-if="isMessagePrivate"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="lock-closed"
/>
<fluent-icon
v-else-if="messageByAgent"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="arrow-reply"
/>
<fluent-icon
v-else-if="isMessageAnActivity"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="info"
/>
</template>
<span v-if="message.content">
{{ parsedLastMessage }}
</span>
<span v-else-if="message.attachments">
<fluent-icon
v-if="attachmentIcon && showMessageType"
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ $t(`${attachmentMessageContent}`) }}
</span>
<span v-else>
{{ $t('CHAT_LIST.NO_CONTENT') }}
</span>
</span>
</template>
<script>
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
export default {
name: 'MessagePreview',
mixins: [messageFormatterMixin],
props: {
message: {
type: Object,
required: true,
},
showMessageType: {
type: Boolean,
default: true,
},
},
computed: {
messageByAgent() {
const { message_type: messageType } = this.message;
return messageType === MESSAGE_TYPE.OUTGOING;
},
isMessageAnActivity() {
const { message_type: messageType } = this.message;
return messageType === MESSAGE_TYPE.ACTIVITY;
},
isMessagePrivate() {
const { private: isPrivate } = this.message;
return isPrivate;
},
parsedLastMessage() {
const { content_attributes: contentAttributes } = this.message;
const { email: { subject } = {} } = contentAttributes || {};
return this.getPlainText(subject || this.message.content);
},
lastMessageFileType() {
const [{ file_type: fileType } = {}] = this.message.attachments;
return fileType;
},
attachmentIcon() {
return ATTACHMENT_ICONS[this.lastMessageFileType];
},
attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
},
},
};
</script>
@@ -34,6 +34,7 @@
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-chat="currentChat"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
@@ -55,6 +56,8 @@
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-chat="currentChat"
/>
<conversation-label-suggestion
v-if="shouldShowLabelSuggestions"
@@ -21,8 +21,7 @@
<div class="reply-box__top">
<reply-to-message
v-if="shouldShowReplyToMessage"
:message-id="inReplyTo.id"
:message-content="inReplyTo.content"
:message="inReplyTo"
@dismiss="resetReplyToMessage"
@navigate-to-message="navigateToMessage"
/>
@@ -1,29 +1,12 @@
<script setup>
import { computed } from 'vue';
import { extractTextFromMarkdown } from 'dashboard/helper/editorHelper';
const { messageContent } = defineProps({
messageId: {
type: Number,
required: true,
},
messageContent: {
type: String,
default: '',
},
});
const cleanedContent = computed(() => extractTextFromMarkdown(messageContent));
</script>
<template>
<div
class="reply-editor bg-slate-50 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2 cursor-pointer"
@click="$emit('navigate-to-message', messageId)"
>
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" icon-size="14" />
<div class="flex-grow overflow-hidden text-ellipsis">
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }} {{ cleanedContent }}.
<div class="flex items-center flex-grow gap-1 text-xs">
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }}
<message-preview :message="message" :show-message-type="false" />
</div>
<woot-button
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
@@ -37,6 +20,20 @@ const cleanedContent = computed(() => extractTextFromMarkdown(messageContent));
</div>
</template>
<script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
export default {
components: { MessagePreview },
props: {
message: {
type: Object,
required: true,
},
},
};
</script>
<style lang="scss">
// TODO: Remove this
// override for dashboard/assets/scss/widgets/_reply-box.scss
@@ -0,0 +1,69 @@
<template>
<div
class="px-2 py-1.5 -mx-2 rounded-md bg-woot-600 min-w-[15rem] mb-2 cursor-pointer"
@click="$emit('click')"
>
<div v-if="content" class="line-clamp-3">
{{ cleanedContent }}
</div>
<div v-else-if="hasAttachments" class="line-clamp-3">
{{ attachmentLabel }}
</div>
<template v-else> {{ defaultEmptyMessage }} </template>
</div>
</template>
<script>
import { extractTextFromMarkdown } from 'dashboard/helper/editorHelper';
export default {
name: 'ReplyTo',
props: {
content: {
type: String,
default: '',
},
attachments: {
type: Array,
default: () => [],
},
defaultEmptyMessage: {
type: String,
required: true,
},
},
computed: {
cleanedContent() {
return extractTextFromMarkdown(this.content);
},
hasAttachments() {
return this.attachments.length > 0;
},
attachmentLabel() {
if (!this.attachments.length) return null;
const firstFileName = this.attachments[0].data_url?.split('/').pop();
if (this.attachments.length > 1) {
if (firstFileName) {
return this.$t(
'CONVERSATION.REPLY_TO_ATTACHMENT.FILE_PLUS_MULTIPLE',
{
first: firstFileName,
count: (this.attachments.length - 1).toLocaleString(),
}
);
}
return this.$t('CONVERSATION.REPLY_TO_ATTACHMENT.COUNT', {
count: this.attachments.length.toLocaleString(),
});
}
return (
firstFileName ?? this.$t('CONVERSATION.REPLY_TO_ATTACHMENT.ATTACHMENT')
);
},
},
};
</script>
@@ -46,6 +46,12 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Not available, click to scroll to the message",
"REPLY_TO_ATTACHMENT": {
"FILE_PLUS_MULTIPLE": "%{first} and %{count} others",
"COUNT": "%{count} files",
"ATTACHMENT": "Attachment"
},
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -146,3 +146,12 @@ export const MESSAGE_VARIABLES = [
key: 'agent.email',
},
];
export const ATTACHMENT_ICONS = {
image: 'image',
audio: 'headphones-sound-wave',
video: 'video',
file: 'document',
location: 'location',
fallback: 'link',
};