[Enhancement] Create EmptyState component (#209)

This commit is contained in:
Pranav Raj S 2019-11-17 13:09:10 +05:30 committed by GitHub
parent 88ac20efb5
commit 1ad36f164f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 316 additions and 240 deletions

View file

@ -1,263 +1,51 @@
<template> <template>
<div :class="conversationClass"> <div :class="conversationClass">
<div v-if="currentChat.id !== null" class="view-box columns"> <messages-view
<conversation-header v-if="currentChat.id"
:chat="currentChat" :inbox-id="inboxId"
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
@contactPanelToggle="onToggleContactPanel" @contactPanelToggle="onToggleContactPanel"
/> >
<ul class="conversation-panel"> </messages-view>
<transition name="slide-up"> <empty-state v-else></empty-state>
<li>
<span v-if="shouldShowSpinner" class="spinner message" />
</li>
</transition>
<conversation
v-for="message in getReadMessages"
:key="message.id"
:data="message"
/>
<li v-show="getUnreadCount != 0" class="unread--toast">
<span>
{{ getUnreadCount }} UNREAD MESSAGE{{
getUnreadCount > 1 ? 'S' : ''
}}
</span>
</li>
<conversation
v-for="message in getUnReadMessages"
:key="message.id"
:data="message"
/>
</ul>
<ReplyBox
:conversation-id="currentChat.id"
@scrollToMessage="focusLastMessage"
/>
</div>
<!-- No Conversation Selected -->
<div v-else class="columns full-height conv-empty-state">
<!-- Loading status -->
<woot-loading-state
v-if="fetchingInboxes || loadingChatList"
:message="loadingIndicatorMessage"
/>
<!-- Show empty state images if not loading -->
<div v-if="!fetchingInboxes && !loadingChatList" class="current-chat">
<!-- No inboxes attached -->
<div v-if="!inboxesList.length">
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
<span v-if="isAdmin()">
{{ $t('CONVERSATION.NO_INBOX_1') }}
<br />
<router-link :to="newInboxURL">
{{ $t('CONVERSATION.CLICK_HERE') }}
</router-link>
{{ $t('CONVERSATION.NO_INBOX_2') }}
</span>
<span v-if="!isAdmin()">
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
</span>
</div>
<!-- No conversations available -->
<div v-else-if="!allConversations.length">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>
{{ $t('CONVERSATION.NO_MESSAGE_1') }}
<br />
<a :href="linkToMessage" target="_blank">
{{ $t('CONVERSATION.CLICK_HERE') }}
</a>
{{ $t('CONVERSATION.NO_MESSAGE_2') }}
</span>
</div>
<!-- No conversation selected -->
<div v-else-if="allConversations.length && currentChat.id === null">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>{{ $t('CONVERSATION.404') }}</span>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
/* eslint no-console: 0 */
/* eslint no-extra-boolean-cast: 0 */
/* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import EmptyState from './EmptyState';
import ConversationHeader from './ConversationHeader'; import MessagesView from './MessagesView';
import ReplyBox from './ReplyBox';
import Conversation from './Conversation';
import conversationMixin from '../../../mixins/conversations';
import adminMixin from '../../../mixins/isAdmin';
import { frontendURL } from '../../../helper/URLHelper';
export default { export default {
components: { components: {
ConversationHeader, EmptyState,
Conversation, MessagesView,
ReplyBox,
}, },
mixins: [conversationMixin, adminMixin],
props: { props: {
inboxId: { inboxId: {
type: [Number, String], type: [Number, String],
required: true, default: '',
required: false,
}, },
isContactPanelOpen: { isContactPanelOpen: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
data() {
return {
isLoadingPrevious: true,
heightBeforeLoad: null,
conversationPanel: null,
};
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'getInboxesList',
listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
fetchingInboxes: 'getInboxLoadingStatus',
loadingChatList: 'getChatListLoadingStatus',
}), }),
conversationClass() { conversationClass() {
return `medium-${ return `medium-${
this.isContactPanelOpen ? '5' : '8' this.isContactPanelOpen ? '5' : '8'
} columns conversation-wrap`; } columns conversation-wrap`;
}, },
// Loading indicator
// Returns corresponding loading message
loadingIndicatorMessage() {
if (this.fetchingInboxes) {
return this.$t('CONVERSATION.LOADING_INBOXES');
}
return this.$t('CONVERSATION.LOADING_CONVERSATIONS');
}, },
getMessages() {
const [chat] = this.allConversations.filter(
c => c.id === this.currentChat.id
);
return chat;
},
// Get current FB Page ID
getPageId() {
let stateInbox;
if (this.inboxId) {
const inboxId = Number(this.inboxId);
[stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
} else {
[stateInbox] = this.inboxesList;
}
return !stateInbox ? 0 : stateInbox.pageId;
},
// Get current FB Page ID link
linkToMessage() {
return `https://m.me/${this.getPageId}`;
},
getReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.readMessages(chat);
},
getUnReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.unReadMessages(chat);
},
shouldShowSpinner() {
return (
this.getMessages.dataFetched === undefined ||
(!this.listLoadingStatus && this.isLoadingPrevious)
);
},
newInboxURL() {
return frontendURL('settings/inboxes/new');
},
shouldLoadMoreChats() {
return !this.listLoadingStatus && !this.isLoadingPrevious;
},
},
created() {
bus.$on('scrollToMessage', () => {
this.focusLastMessage();
this.makeMessagesRead();
});
},
methods: { methods: {
focusLastMessage() {
setTimeout(() => {
this.attachListner();
}, 0);
},
onToggleContactPanel() { onToggleContactPanel() {
this.$emit('contactPanelToggle'); this.$emit('contactPanelToggle');
}, },
attachListner() {
this.conversationPanel = this.$el.querySelector('.conversation-panel');
this.heightBeforeLoad =
this.getUnreadCount === 0
? this.conversationPanel.scrollHeight
: this.$el.querySelector('.conversation-panel .unread--toast')
.offsetTop - 56;
this.conversationPanel.scrollTop = this.heightBeforeLoad;
this.conversationPanel.addEventListener('scroll', this.handleScroll);
this.isLoadingPrevious = false;
},
handleScroll(e) {
const dataFetchCheck =
this.getMessages.dataFetched === true && this.shouldLoadMoreChats;
if (
e.target.scrollTop < 100 &&
!this.isLoadingPrevious &&
dataFetchCheck
) {
this.isLoadingPrevious = true;
this.$store
.dispatch('fetchPreviousMessages', {
conversationId: this.currentChat.id,
before: this.getMessages.messages[0].id,
})
.then(() => {
this.conversationPanel.scrollTop =
this.conversationPanel.scrollHeight -
(this.heightBeforeLoad - this.conversationPanel.scrollTop);
this.isLoadingPrevious = false;
this.heightBeforeLoad =
this.getUnreadCount === 0
? this.conversationPanel.scrollHeight
: this.$el.querySelector('.conversation-panel .unread--toast')
.offsetTop - 56;
});
}
},
makeMessagesRead() {
if (this.getUnreadCount !== 0 && this.getMessages !== undefined) {
this.$store.dispatch('markMessagesRead', {
id: this.currentChat.id,
lastSeen: this.getMessages.messages.last().created_at,
});
}
},
}, },
}; };
</script> </script>

View file

@ -0,0 +1,70 @@
<template>
<div class="columns full-height conv-empty-state">
<woot-loading-state
v-if="fetchingInboxes || loadingChatList"
:message="loadingIndicatorMessage"
/>
<!-- Show empty state images if not loading -->
<div v-if="!fetchingInboxes && !loadingChatList" class="current-chat">
<!-- No inboxes attached -->
<div v-if="!inboxesList.length">
<img src="~dashboard/assets/images/inboxes.svg" alt="No Inboxes" />
<span v-if="isAdmin()">
{{ $t('CONVERSATION.NO_INBOX_1') }}
<br />
<router-link :to="newInboxURL">
{{ $t('CONVERSATION.CLICK_HERE') }}
</router-link>
{{ $t('CONVERSATION.NO_INBOX_2') }}
</span>
<span v-if="!isAdmin()">
{{ $t('CONVERSATION.NO_INBOX_AGENT') }}
</span>
</div>
<!-- No conversations available -->
<div v-else-if="!allConversations.length">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>
{{ $t('CONVERSATION.NO_MESSAGE_1') }}
<br />
</span>
</div>
<!-- No conversation selected -->
<div v-else-if="allConversations.length && currentChat.id === null">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>{{ $t('CONVERSATION.404') }}</span>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import adminMixin from '../../../mixins/isAdmin';
import { frontendURL } from '../../../helper/URLHelper';
export default {
mixins: [adminMixin],
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'getInboxesList',
fetchingInboxes: 'getInboxLoadingStatus',
loadingChatList: 'getChatListLoadingStatus',
}),
loadingIndicatorMessage() {
if (this.fetchingInboxes) {
return this.$t('CONVERSATION.LOADING_INBOXES');
}
return this.$t('CONVERSATION.LOADING_CONVERSATIONS');
},
},
methods: {
newInboxURL() {
return frontendURL('settings/inboxes/new');
},
},
};
</script>

View file

@ -1,30 +1,40 @@
<template> <template>
<li :class="alignBubble" v-if="data.attachment || data.content"> <li v-if="data.attachment || data.content" :class="alignBubble">
<div :class="wrapClass"> <div :class="wrapClass">
<p <p
:class="{ bubble: isBubble, 'is-private': isPrivate }"
v-tooltip.top-start="sentByMessage" v-tooltip.top-start="sentByMessage"
:class="{ bubble: isBubble, 'is-private': isPrivate }"
> >
<bubble-image <bubble-image
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"
v-if="data.attachment && data.attachment.file_type==='image'"
/> />
<bubble-audio <bubble-audio
v-if="data.attachment && data.attachment.file_type === 'audio'"
:url="data.attachment.data_url" :url="data.attachment.data_url"
:readable-time="readableTime" :readable-time="readableTime"
v-if="data.attachment && data.attachment.file_type==='audio'"
/> />
<bubble-map <bubble-map
v-if="data.attachment && data.attachment.file_type === 'location'"
:lat="data.attachment.coordinates_lat" :lat="data.attachment.coordinates_lat"
:lng="data.attachment.coordinates_long" :lng="data.attachment.coordinates_long"
:label="data.attachment.fallback_title" :label="data.attachment.fallback_title"
:readable-time="readableTime" :readable-time="readableTime"
v-if="data.attachment && data.attachment.file_type==='location'"
/> />
<i class="icon ion-person" v-if="data.message_type === 2"></i> <i v-if="data.message_type === 2" class="icon ion-person"></i>
<bubble-text v-if="data.content" :message="message" :readable-time="readableTime"/> <bubble-text
<i class="icon ion-android-lock" v-if="isPrivate" v-tooltip.top-start="toolTipMessage" @mouseenter="isHovered = true" @mouseleave="isHovered = false"></i> v-if="data.content"
:message="message"
:readable-time="readableTime"
/>
<i
v-if="isPrivate"
v-tooltip.top-start="toolTipMessage"
class="icon ion-android-lock"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
></i>
</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 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"> -->
@ -46,8 +56,13 @@ export default {
BubbleMap, BubbleMap,
BubbleAudio, BubbleAudio,
}, },
props: ['data'],
mixins: [timeMixin], mixins: [timeMixin],
props: {
data: {
type: Object,
required: true,
},
},
data() { data() {
return { return {
isHovered: false, isHovered: false,
@ -71,11 +86,16 @@ export default {
return this.data.private; return this.data.private;
}, },
toolTipMessage() { toolTipMessage() {
return this.data.private ? { content: this.$t('CONVERSATION.VISIBLE_TO_AGENTS'), classes: 'top' } : false; return this.data.private
? { content: this.$t('CONVERSATION.VISIBLE_TO_AGENTS'), classes: 'top' }
: false;
}, },
sentByMessage() { sentByMessage() {
return this.data.message_type === 1 && !this.isHovered && this.data.sender !== undefined ? return this.data.message_type === 1 &&
{ content: `Sent by: ${this.data.sender.name}`, classes: 'top' } : false; !this.isHovered &&
this.data.sender !== undefined
? { content: `Sent by: ${this.data.sender.name}`, classes: 'top' }
: false;
}, },
wrapClass() { wrapClass() {
return { return {
@ -83,14 +103,16 @@ export default {
'activity-wrap': !this.isBubble, 'activity-wrap': !this.isBubble,
}; };
}, },
}, },
methods: { methods: {
getEmojiSVG, getEmojiSVG,
linkify(text) { linkify(text) {
if (!text) return text; if (!text) return text;
const urlRegex = /(https?:\/\/[^\s]+)/g; const urlRegex = /(https?:\/\/[^\s]+)/g;
return text.replace(urlRegex, url => `<a href="${url}" target="_blank">${url}</a>`); return text.replace(
urlRegex,
url => `<a href="${url}" target="_blank">${url}</a>`
);
}, },
}, },
}; };

View file

@ -0,0 +1,196 @@
<template>
<div class="view-box columns">
<conversation-header
:chat="currentChat"
:is-contact-panel-open="isContactPanelOpen"
@contactPanelToggle="onToggleContactPanel"
/>
<ul class="conversation-panel">
<transition name="slide-up">
<li>
<span v-if="shouldShowSpinner" class="spinner message" />
</li>
</transition>
<message
v-for="message in getReadMessages"
:key="message.id"
:data="message"
/>
<li v-show="getUnreadCount != 0" class="unread--toast">
<span>
{{ getUnreadCount }} UNREAD MESSAGE{{ getUnreadCount > 1 ? 'S' : '' }}
</span>
</li>
<message
v-for="message in getUnReadMessages"
:key="message.id"
:data="message"
/>
</ul>
<ReplyBox
:conversation-id="currentChat.id"
@scrollToMessage="focusLastMessage"
/>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import ConversationHeader from './ConversationHeader';
import ReplyBox from './ReplyBox';
import Message from './Message';
import conversationMixin from '../../../mixins/conversations';
export default {
components: {
ConversationHeader,
Message,
ReplyBox,
},
mixins: [conversationMixin],
props: {
inboxId: {
type: [Number, String],
required: true,
},
isContactPanelOpen: {
type: Boolean,
default: false,
},
},
data() {
return {
isLoadingPrevious: true,
heightBeforeLoad: null,
conversationPanel: null,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'getInboxesList',
listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
fetchingInboxes: 'getInboxLoadingStatus',
loadingChatList: 'getChatListLoadingStatus',
}),
getMessages() {
const [chat] = this.allConversations.filter(
c => c.id === this.currentChat.id
);
return chat;
},
// Get current FB Page ID
getPageId() {
let stateInbox;
if (this.inboxId) {
const inboxId = Number(this.inboxId);
[stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
} else {
[stateInbox] = this.inboxesList;
}
return !stateInbox ? 0 : stateInbox.pageId;
},
// Get current FB Page ID link
linkToMessage() {
return `https://m.me/${this.getPageId}`;
},
getReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.readMessages(chat);
},
getUnReadMessages() {
const chat = this.getMessages;
return chat === undefined ? null : this.unReadMessages(chat);
},
shouldShowSpinner() {
return (
this.getMessages.dataFetched === undefined ||
(!this.listLoadingStatus && this.isLoadingPrevious)
);
},
shouldLoadMoreChats() {
return !this.listLoadingStatus && !this.isLoadingPrevious;
},
},
created() {
bus.$on('scrollToMessage', () => {
this.focusLastMessage();
this.makeMessagesRead();
});
},
methods: {
focusLastMessage() {
setTimeout(() => {
this.attachListner();
}, 0);
},
onToggleContactPanel() {
this.$emit('contactPanelToggle');
},
attachListner() {
this.conversationPanel = this.$el.querySelector('.conversation-panel');
this.heightBeforeLoad =
this.getUnreadCount === 0
? this.conversationPanel.scrollHeight
: this.$el.querySelector('.conversation-panel .unread--toast')
.offsetTop - 56;
this.conversationPanel.scrollTop = this.heightBeforeLoad;
this.conversationPanel.addEventListener('scroll', this.handleScroll);
this.isLoadingPrevious = false;
},
handleScroll(e) {
const dataFetchCheck =
this.getMessages.dataFetched === true && this.shouldLoadMoreChats;
if (
e.target.scrollTop < 100 &&
!this.isLoadingPrevious &&
dataFetchCheck
) {
this.isLoadingPrevious = true;
this.$store
.dispatch('fetchPreviousMessages', {
conversationId: this.currentChat.id,
before: this.getMessages.messages[0].id,
})
.then(() => {
this.conversationPanel.scrollTop =
this.conversationPanel.scrollHeight -
(this.heightBeforeLoad - this.conversationPanel.scrollTop);
this.isLoadingPrevious = false;
this.heightBeforeLoad =
this.getUnreadCount === 0
? this.conversationPanel.scrollHeight
: this.$el.querySelector('.conversation-panel .unread--toast')
.offsetTop - 56;
});
}
},
makeMessagesRead() {
if (this.getUnreadCount !== 0 && this.getMessages !== undefined) {
this.$store.dispatch('markMessagesRead', {
id: this.currentChat.id,
lastSeen: this.getMessages.messages.last().created_at,
});
}
},
},
};
</script>