Move src to dashboard (#152)
This commit is contained in:
parent
012a2743f2
commit
2783fb6006
187 changed files with 29 additions and 29 deletions
177
app/javascript/dashboard/components/ChatList.vue
Normal file
177
app/javascript/dashboard/components/ChatList.vue
Normal file
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<div class="conversations-sidebar medium-4 columns">
|
||||
<!-- <SearchBox></SearchBox> -->
|
||||
|
||||
<div class="chat-list__top">
|
||||
<h1 class="page-title">
|
||||
{{ getInboxName }}
|
||||
</h1>
|
||||
<chat-filter @statusFilterChange="getDataForStatusTab"></chat-filter>
|
||||
</div>
|
||||
|
||||
<chat-type-tabs
|
||||
:items="assigneeTabItems"
|
||||
:active-tab-index="activeAssigneeTab"
|
||||
class="tab--chat-type"
|
||||
@chatTabChange="getDataForTab"
|
||||
></chat-type-tabs>
|
||||
|
||||
<p
|
||||
v-if="!chatListLoading && !getChatsForTab(activeStatusTab).length"
|
||||
class="content-box"
|
||||
>
|
||||
{{ $t('CHAT_LIST.LIST.404') }}
|
||||
</p>
|
||||
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
<span class="spinner message"></span>
|
||||
</div>
|
||||
|
||||
<transition-group
|
||||
name="conversations-list"
|
||||
tag="div"
|
||||
class="conversations-list"
|
||||
>
|
||||
<conversation-card
|
||||
v-for="chat in getChatsForTab(activeStatusTab)"
|
||||
:key="chat.id"
|
||||
:chat="chat"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-env browser */
|
||||
/* eslint no-console: 0 */
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ChatFilter from './widgets/conversation/ChatFilter';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs';
|
||||
import ConversationCard from './widgets/conversation/ConversationCard';
|
||||
import timeMixin from '../mixins/time';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from '../constants';
|
||||
|
||||
export default {
|
||||
mixins: [timeMixin, conversationMixin],
|
||||
props: ['conversationInbox', 'pageTitle'],
|
||||
data: () => ({
|
||||
chats: null,
|
||||
activeStatusTab: 0,
|
||||
activeAssigneeTab: 0,
|
||||
toggleType: true,
|
||||
allMessageType: 2,
|
||||
}),
|
||||
mounted() {
|
||||
this.$watch('$store.state.route', () => {
|
||||
if (this.$store.state.route.name !== 'inbox_conversation') {
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchData();
|
||||
}
|
||||
});
|
||||
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchData();
|
||||
this.$store.dispatch('fetchAgents');
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatLists: 'getAllConversations',
|
||||
mineChatsList: 'getMineChats',
|
||||
allChatList: 'getAllStatusChats',
|
||||
unAssignedChatsList: 'getUnAssignedChats',
|
||||
inboxesList: 'getInboxesList',
|
||||
chatListLoading: 'getChatListLoadingStatus',
|
||||
currentUserID: 'getCurrentUserID',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
}),
|
||||
convStats() {
|
||||
const mineCount = this.mineChatsList.length;
|
||||
const unAssignedCount = this.unAssignedChatsList.length;
|
||||
const allCount = this.allChatList.length;
|
||||
return {
|
||||
mineCount,
|
||||
unAssignedCount,
|
||||
allCount,
|
||||
};
|
||||
},
|
||||
assigneeTabItems() {
|
||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map((item, index) => ({
|
||||
id: index,
|
||||
name: item.NAME,
|
||||
count: this.convStats[item.KEY] || 0,
|
||||
}));
|
||||
},
|
||||
getInboxName() {
|
||||
const inboxId = Number(this.activeInbox);
|
||||
const [stateInbox] = this.inboxesList.filter(
|
||||
inbox => inbox.channel_id === inboxId
|
||||
);
|
||||
return !stateInbox ? this.pageTitle : stateInbox.label;
|
||||
},
|
||||
getToggleStatus() {
|
||||
if (this.toggleType) {
|
||||
return 'Open';
|
||||
}
|
||||
return 'Resolved';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchData() {
|
||||
if (this.chatLists.length === 0) {
|
||||
this.$store.dispatch('fetchAllConversations', {
|
||||
inbox: this.conversationInbox,
|
||||
assigneeStatus: this.allMessageType,
|
||||
convStatus: this.activeStatusTab,
|
||||
});
|
||||
}
|
||||
},
|
||||
getDataForTab(index) {
|
||||
this.activeAssigneeTab = index;
|
||||
if (!(index in this.chatLists)) {
|
||||
// this.$store.dispatch('fetchList', {
|
||||
// inbox: this.conversationInbox,
|
||||
// type: index,
|
||||
// });
|
||||
}
|
||||
},
|
||||
getDataForStatusTab(index) {
|
||||
this.activeStatusTab = index;
|
||||
},
|
||||
getChatsForTab() {
|
||||
let copyList = [];
|
||||
if (this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.MINE) {
|
||||
copyList = this.mineChatsList.slice();
|
||||
} else if (
|
||||
this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE_SLUG.UNASSIGNED
|
||||
) {
|
||||
copyList = this.unAssignedChatsList.slice();
|
||||
} else {
|
||||
copyList = this.allChatList.slice();
|
||||
}
|
||||
const sorted = copyList.sort((a, b) =>
|
||||
this.wootTime(this.lastMessage(a).created_at).isBefore(
|
||||
this.wootTime(this.lastMessage(b).created_at)
|
||||
)
|
||||
);
|
||||
|
||||
return sorted;
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ChatTypeTabs,
|
||||
ConversationCard,
|
||||
ChatFilter,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
.spinner {
|
||||
margin-top: $space-normal;
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
</style>
|
33
app/javascript/dashboard/components/Modal.vue
Normal file
33
app/javascript/dashboard/components/Modal.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<transition name="modal-fade">
|
||||
<div class="modal-mask" @click="close" v-if="show" transition="modal">
|
||||
<div class="modal-container" :class="className" @click.stop>
|
||||
<i class="ion-android-close modal--close" @click="close"></i>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
show: Boolean,
|
||||
onClose: Function,
|
||||
className: String,
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.onClose();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (this.show && e.keyCode === 27) {
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
21
app/javascript/dashboard/components/ModalHeader.vue
Normal file
21
app/javascript/dashboard/components/ModalHeader.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="column page-top-bar">
|
||||
<img :src="headerImage" alt="No image" v-if="headerImage"/>
|
||||
<h2 class="page-sub-title">
|
||||
{{headerTitle}}
|
||||
</h2>
|
||||
<p class="small-12 column">
|
||||
{{headerContent}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: String,
|
||||
headerContent: String,
|
||||
headerImage: String,
|
||||
},
|
||||
};
|
||||
</script>
|
30
app/javascript/dashboard/components/Snackbar.vue
Normal file
30
app/javascript/dashboard/components/Snackbar.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="ui-snackbar">
|
||||
<div class="ui-snackbar-text">{{ message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
message: String,
|
||||
showButton: Boolean,
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
default: 3000,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleAfterTimeout: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
};
|
||||
</script>
|
37
app/javascript/dashboard/components/SnackbarContainer.vue
Normal file
37
app/javascript/dashboard/components/SnackbarContainer.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<transition-group name="toast-fade" tag="div" class="ui-snackbar-container">
|
||||
<woot-snackbar :message="snackMessage" v-for="snackMessage in snackMessages" v-bind:key="snackMessage"></woot-snackbar>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import WootSnackbar from './Snackbar';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
duration: {
|
||||
default: 2500,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
snackMessages: [],
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.$on('newToastMessage', (message) => {
|
||||
this.snackMessages.push(message);
|
||||
window.setTimeout(() => {
|
||||
this.snackMessages.splice(0, 1);
|
||||
}, this.duration);
|
||||
});
|
||||
},
|
||||
|
||||
components: {
|
||||
WootSnackbar,
|
||||
},
|
||||
};
|
||||
</script>
|
3
app/javascript/dashboard/components/Spinner.vue
Normal file
3
app/javascript/dashboard/components/Spinner.vue
Normal file
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span class="spinner small"></span>
|
||||
</template>
|
19
app/javascript/dashboard/components/Thumbnail.vue
Normal file
19
app/javascript/dashboard/components/Thumbnail.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<div class="profile-card">
|
||||
<div class="profile-image" v-bind:style="{ height: size, width: size }">
|
||||
<img v-bind:src="src">
|
||||
</div>
|
||||
<img class="source-badge" src="../assets/images/fb-badge.png" v-if="badge === 'fb'">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* Thumbnail Component
|
||||
* Src - source for round image
|
||||
* Size - Size of the thumbnail
|
||||
* Badge - Chat source indication { fb / telegram }
|
||||
*/
|
||||
export default {
|
||||
props: ['src', 'size', 'badge'],
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<button type="submit" :disabled="disabled" :class="computedClass">
|
||||
<i v-if="!!iconClass" :class="iconClass" class="icon"></i>
|
||||
<span>{{ buttonText }}</span>
|
||||
<spinner v-if="loading" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedClass() {
|
||||
return `button nice ${this.buttonClass || ' '}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="button nice resolve--button"
|
||||
:class="buttonClass"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
<i v-if="!isLoading" class="icon" :class="buttonIconClass"></i>
|
||||
<spinner v-if="isLoading" />
|
||||
{{ currentStatus }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
export default {
|
||||
props: ['conversationId'],
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
currentStatus() {
|
||||
const ButtonName = this.currentChat.status === 0 ? 'Resolve' : 'Reopen';
|
||||
return ButtonName;
|
||||
},
|
||||
buttonClass() {
|
||||
return this.currentChat.status === 0 ? 'success' : 'warning';
|
||||
},
|
||||
buttonIconClass() {
|
||||
return this.currentChat.status === 0 ? 'ion-checkmark' : 'ion-refresh';
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
methods: {
|
||||
toggleStatus() {
|
||||
this.isLoading = true;
|
||||
this.$store.dispatch('toggleStatus', this.currentChat.id).then(() => {
|
||||
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
40
app/javascript/dashboard/components/index.js
Normal file
40
app/javascript/dashboard/components/index.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/* eslint no-plusplus: 0 */
|
||||
/* eslint-env browser */
|
||||
|
||||
import Modal from './Modal';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import Spinner from './Spinner';
|
||||
import SubmitButton from './buttons/FormSubmitButton';
|
||||
import Tabs from './ui/Tabs/Tabs';
|
||||
import TabsItem from './ui/Tabs/TabsItem';
|
||||
import LoadingState from './widgets/LoadingState';
|
||||
import ReportStatsCard from './widgets/ReportStatsCard';
|
||||
import Bar from './widgets/chart/BarChart';
|
||||
import ModalHeader from './ModalHeader';
|
||||
|
||||
const WootUIKit = {
|
||||
Modal,
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
SubmitButton,
|
||||
Tabs,
|
||||
TabsItem,
|
||||
LoadingState,
|
||||
ReportStatsCard,
|
||||
Bar,
|
||||
ModalHeader,
|
||||
install(Vue) {
|
||||
const keys = Object.keys(this);
|
||||
keys.pop(); // remove 'install' from keys
|
||||
let i = keys.length;
|
||||
while (i--) {
|
||||
Vue.component(`woot${keys[i]}`, this[keys[i]]);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined' && window.Vue) {
|
||||
window.Vue.use(WootUIKit);
|
||||
}
|
||||
|
||||
export default WootUIKit;
|
159
app/javascript/dashboard/components/layout/Sidebar.vue
Normal file
159
app/javascript/dashboard/components/layout/Sidebar.vue
Normal file
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<aside class="sidebar animated shrink columns">
|
||||
<div class="logo">
|
||||
<router-link :to="dashboardPath" replace>
|
||||
<img src="~dashboard/assets/images/woot-logo.svg" alt="Woot-logo" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="main-nav">
|
||||
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
||||
<sidebar-item
|
||||
v-for="item in accessibleMenuItems"
|
||||
:key="item.toState"
|
||||
:menu-item="item"
|
||||
/>
|
||||
</transition-group>
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<woot-status-bar
|
||||
v-if="shouldShowStatusBox"
|
||||
:message="trialMessage"
|
||||
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
|
||||
:button-route="{ name: 'billing' }"
|
||||
:type="statusBarClass"
|
||||
:show-button="isAdmin()"
|
||||
/>
|
||||
</transition>
|
||||
<div class="bottom-nav">
|
||||
<transition name="menu-slide">
|
||||
<div
|
||||
v-if="showOptionsMenu"
|
||||
v-on-clickaway="showOptions"
|
||||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<!-- <li><a href="#">Help & Support</a></li> -->
|
||||
<li><a href="#" @click.prevent="logout()">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="current-user" @click.prevent="showOptions()">
|
||||
<img class="current-user--thumbnail" :src="gravatarUrl()" />
|
||||
<div class="current-user--data">
|
||||
<h3 class="current-user--name">
|
||||
{{ currentUser.name }}
|
||||
</h3>
|
||||
<h5 class="current-user--role">
|
||||
{{ currentUser.role }}
|
||||
</h5>
|
||||
</div>
|
||||
<span
|
||||
class="current-user--options icon ion-android-more-vertical"
|
||||
></span>
|
||||
</div>
|
||||
<!-- <router-link class="icon ion-arrow-graph-up-right" tag="span" to="/settings/reports" active-class="active"></router-link> -->
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import md5 from 'md5';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import Auth from '../../api/auth';
|
||||
import SidebarItem from './SidebarItem';
|
||||
import WootStatusBar from '../widgets/StatusBar';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
mixins: [clickaway, adminMixin],
|
||||
props: {
|
||||
route: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// this.$store.dispatch('fetchLabels');
|
||||
this.$store.dispatch('fetchInboxes');
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
sidebarList: 'getMenuItems',
|
||||
daysLeft: 'getTrialLeft',
|
||||
subscriptionData: 'getSubscription',
|
||||
}),
|
||||
accessibleMenuItems() {
|
||||
const currentRoute = this.$store.state.route.name;
|
||||
// get all keys in menuGroup
|
||||
const groupKey = Object.keys(this.sidebarList);
|
||||
|
||||
let menuItems = [];
|
||||
// Iterate over menuGroup to find the correct group
|
||||
for (let i = 0; i < groupKey.length; i += 1) {
|
||||
const groupItem = this.sidebarList[groupKey[i]];
|
||||
// Check if current route is included
|
||||
const isRouteIncluded = groupItem.routes.indexOf(currentRoute) > -1;
|
||||
if (isRouteIncluded) {
|
||||
menuItems = Object.values(groupItem.menuItems);
|
||||
}
|
||||
}
|
||||
|
||||
const { role } = this.currentUser;
|
||||
return menuItems.filter(
|
||||
menuItem =>
|
||||
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
|
||||
);
|
||||
},
|
||||
dashboardPath() {
|
||||
return frontendURL('dashboard');
|
||||
},
|
||||
currentUser() {
|
||||
return Auth.getCurrentUser();
|
||||
},
|
||||
trialMessage() {
|
||||
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
|
||||
},
|
||||
shouldShowStatusBox() {
|
||||
return (
|
||||
this.subscriptionData.state === 'trial' ||
|
||||
this.subscriptionData.state === 'cancelled'
|
||||
);
|
||||
},
|
||||
statusBarClass() {
|
||||
if (this.subscriptionData.state === 'trial') {
|
||||
return 'warning';
|
||||
}
|
||||
if (this.subscriptionData.state === 'cancelled') {
|
||||
return 'danger';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
logout() {
|
||||
Auth.logout();
|
||||
},
|
||||
gravatarUrl() {
|
||||
const hash = md5(this.currentUser.email);
|
||||
return `${window.WootConstants.GRAVATAR_URL}${hash}?d=monsterid`;
|
||||
},
|
||||
showOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
SidebarItem,
|
||||
WootStatusBar,
|
||||
},
|
||||
};
|
||||
</script>
|
91
app/javascript/dashboard/components/layout/SidebarItem.vue
Normal file
91
app/javascript/dashboard/components/layout/SidebarItem.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<router-link
|
||||
:to="menuItem.toState"
|
||||
tag="li"
|
||||
active-class="active"
|
||||
:class="computedClass"
|
||||
>
|
||||
<a
|
||||
:class="getMenuItemClass"
|
||||
data-tooltip
|
||||
aria-haspopup="true"
|
||||
:title="menuItem.toolTip"
|
||||
>
|
||||
<i :class="menuItem.icon" />
|
||||
{{ menuItem.label }}
|
||||
<span
|
||||
v-if="showItem(menuItem)"
|
||||
class="ion-ios-plus-outline"
|
||||
@click.prevent="newLinkClick"
|
||||
/>
|
||||
</a>
|
||||
<ul v-if="menuItem.hasSubMenu" class="nested vertical menu">
|
||||
<router-link
|
||||
v-for="child in menuItem.children"
|
||||
:key="child.label"
|
||||
active-class="active flex-container"
|
||||
:class="computedInboxClass(child)"
|
||||
tag="li"
|
||||
:to="child.toState"
|
||||
>
|
||||
<a>{{ child.label }}</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import router from '../../routes';
|
||||
import auth from '../../api/auth';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
activeInbox: 'getSelectedInbox',
|
||||
}),
|
||||
getMenuItemClass() {
|
||||
return this.menuItem.cssClass
|
||||
? `side-menu ${this.menuItem.cssClass}`
|
||||
: 'side-menu';
|
||||
},
|
||||
computedClass() {
|
||||
// If active Inbox is present
|
||||
// donot highlight conversations
|
||||
if (this.activeInbox) return ' ';
|
||||
|
||||
if (
|
||||
this.$store.state.route.name === 'inbox_conversation' &&
|
||||
this.menuItem.toStateName === 'home'
|
||||
) {
|
||||
return 'active';
|
||||
}
|
||||
return ' ';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
computedInboxClass(child) {
|
||||
if (parseInt(this.activeInbox, 10) === child.channel_id) {
|
||||
return 'active flex-container';
|
||||
}
|
||||
return ' ';
|
||||
},
|
||||
newLinkClick() {
|
||||
router.push({ name: 'settings_inbox_new', params: { page: 'new' } });
|
||||
},
|
||||
showItem(item) {
|
||||
return auth.isAdmin() && item.newLink !== undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
49
app/javascript/dashboard/components/ui/Switch.vue
Normal file
49
app/javascript/dashboard/components/ui/Switch.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
|
||||
<template>
|
||||
<label class="switch" :class="classObject">
|
||||
<input class="switch-input" :name="name" :id="id" :disabled="disabled" v-model="value" type="checkbox">
|
||||
<div class="switch-paddle" :for="name">
|
||||
<span class="show-for-sr">on off</span>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
isFullwidth: Boolean,
|
||||
type: String,
|
||||
size: String,
|
||||
checked: Boolean,
|
||||
name: String,
|
||||
id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: null,
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
this.value = this.checked;
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('input', this.value = !!this.checked);
|
||||
},
|
||||
computed: {
|
||||
classObject() {
|
||||
const { type, size, value } = this;
|
||||
return {
|
||||
[`is-${type}`]: type,
|
||||
[`${size}`]: size,
|
||||
checked: value,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
33
app/javascript/dashboard/components/ui/Tabs/Tabs.js
Normal file
33
app/javascript/dashboard/components/ui/Tabs/Tabs.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
||||
|
||||
export default {
|
||||
name: 'WootTabs',
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
const Tabs = this.$slots.default
|
||||
.filter(
|
||||
node =>
|
||||
node.componentOptions &&
|
||||
node.componentOptions.tag === 'woot-tabs-item'
|
||||
)
|
||||
.map((node, index) => {
|
||||
const data = node.componentOptions.propsData;
|
||||
data.index = index;
|
||||
return node;
|
||||
});
|
||||
return (
|
||||
<ul
|
||||
class={{
|
||||
tabs: true,
|
||||
}}
|
||||
>
|
||||
{Tabs}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
};
|
88
app/javascript/dashboard/components/ui/Tabs/TabsItem.js
Normal file
88
app/javascript/dashboard/components/ui/Tabs/TabsItem.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
||||
/* eslint prefer-template: 0 */
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint func-names: 0 */
|
||||
import TWEEN from 'tween.js';
|
||||
|
||||
export default {
|
||||
name: 'WootTabsItem',
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
animatedNumber: 0,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
active() {
|
||||
return this.index === this.$parent.index;
|
||||
},
|
||||
|
||||
getItemCount() {
|
||||
return this.animatedNumber || this.count;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
count(newValue, oldValue) {
|
||||
let animationFrame;
|
||||
const animate = time => {
|
||||
TWEEN.update(time);
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
};
|
||||
const that = this;
|
||||
new TWEEN.Tween({ tweeningNumber: oldValue })
|
||||
.easing(TWEEN.Easing.Quadratic.Out)
|
||||
.to({ tweeningNumber: newValue }, 500)
|
||||
.onUpdate(function() {
|
||||
that.animatedNumber = this.tweeningNumber.toFixed(0);
|
||||
})
|
||||
.onComplete(() => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
})
|
||||
.start();
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
},
|
||||
},
|
||||
|
||||
render(h) {
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
'tabs-title': true,
|
||||
'is-active': this.active,
|
||||
'uk-disabled': this.disabled,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
on-click={event => {
|
||||
event.preventDefault();
|
||||
if (!this.disabled) {
|
||||
this.$parent.$emit('change', this.index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{`${this.name} (${this.getItemCount})`}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
};
|
58
app/javascript/dashboard/components/ui/Wizard.vue
Normal file
58
app/javascript/dashboard/components/ui/Wizard.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<transition-group
|
||||
name="wizard-items"
|
||||
tag="div"
|
||||
class="wizard-box flex-child-shrink"
|
||||
:class="classObject"
|
||||
>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.route"
|
||||
class="item"
|
||||
:class="{ active: isActive(item), over: isOver(item) }"
|
||||
>
|
||||
<h3>
|
||||
{{ item.title }}
|
||||
<span v-if="isOver(item)" class="completed">
|
||||
<i class="ion-checkmark"></i>
|
||||
</span>
|
||||
</h3>
|
||||
<span class="step">
|
||||
{{ items.indexOf(item) + 1 }}
|
||||
</span>
|
||||
<p>{{ item.body }}</p>
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
isFullwidth: Boolean,
|
||||
},
|
||||
|
||||
computed: {
|
||||
classObject() {
|
||||
return 'full-width';
|
||||
},
|
||||
activeIndex() {
|
||||
return this.items.findIndex(i => i.route === this.$route.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(item) {
|
||||
return this.items.indexOf(item) === this.activeIndex;
|
||||
},
|
||||
isOver(item) {
|
||||
return this.items.indexOf(item) < this.activeIndex;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
14
app/javascript/dashboard/components/widgets/BackButton.vue
Normal file
14
app/javascript/dashboard/components/widgets/BackButton.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">Back</span>
|
||||
</template>
|
||||
<script>
|
||||
import router from '../../routes/index';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
goBack() {
|
||||
router.go(-1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
23
app/javascript/dashboard/components/widgets/ChannelItem.vue
Normal file
23
app/javascript/dashboard/components/widgets/ChannelItem.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="small-3 columns channel" :class="{ inactive: channel !== 'facebook' }" @click.capture="itemClick">
|
||||
<img src="~dashboard/assets/images/channels/facebook.png" v-if="channel === 'facebook'">
|
||||
<img src="~dashboard/assets/images/channels/twitter.png" v-if="channel === 'twitter'">
|
||||
<img src="~dashboard/assets/images/channels/telegram.png" v-if="channel === 'telegram'">
|
||||
<img src="~dashboard/assets/images/channels/line.png" v-if="channel === 'line'">
|
||||
<h3 class="channel__title">{{channel}}</h3>
|
||||
<!-- <p>This is the most sexiest integration to begin </p> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* global bus */
|
||||
export default {
|
||||
props: ['channel'],
|
||||
created() {
|
||||
},
|
||||
methods: {
|
||||
itemClick() {
|
||||
bus.$emit('channelItemClick', this.channel);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
40
app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
Normal file
40
app/javascript/dashboard/components/widgets/ChatTypeTabs.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<woot-tabs :index="tabsIndex" @change="onTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="item in items"
|
||||
:key="item.name"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeTabIndex: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabsIndex: 0,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.tabsIndex = this.activeTabIndex;
|
||||
},
|
||||
methods: {
|
||||
onTabChange(selectedTabIndex) {
|
||||
this.$emit('chatTabChange', selectedTabIndex);
|
||||
this.tabsIndex = selectedTabIndex;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
17
app/javascript/dashboard/components/widgets/EmptyState.vue
Normal file
17
app/javascript/dashboard/components/widgets/EmptyState.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div class="row empty-state">
|
||||
<h3 class="title">{{title}}</h3>
|
||||
<p class="message">{{message}}</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
message: String,
|
||||
buttonText: String,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="inbox-item" >
|
||||
<img src="~dashboard/assets/images/no_page_image.png" alt="No Page Image"/>
|
||||
<div class="item--details columns">
|
||||
<h4 class="item--name">{{ inbox.label }}</h4>
|
||||
<p class="item--sub">Facebook</p>
|
||||
</div>
|
||||
<!-- <span class="ion-chevron-right arrow"></span> -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* global bus */
|
||||
// import WootSwitch from '../ui/Switch';
|
||||
|
||||
export default {
|
||||
props: ['inbox'],
|
||||
created() {
|
||||
},
|
||||
};
|
||||
</script>
|
12
app/javascript/dashboard/components/widgets/LoadingState.vue
Normal file
12
app/javascript/dashboard/components/widgets/LoadingState.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="row loading-state">
|
||||
<h6 class="message">{{message}}<span class="spinner"></span></h6>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: String,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="small-2 report-card" :class="{ 'active': selected }" v-on:click="onClick(index)">
|
||||
<h3 class="heading">{{heading}}</h3>
|
||||
<h4 class="metric">{{point}}</h4>
|
||||
<p class="desc">{{desc}}</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
heading: String,
|
||||
point: [Number, String],
|
||||
index: Number,
|
||||
desc: String,
|
||||
selected: Boolean,
|
||||
onClick: Function,
|
||||
},
|
||||
};
|
||||
</script>
|
11
app/javascript/dashboard/components/widgets/SearchBox.vue
Normal file
11
app/javascript/dashboard/components/widgets/SearchBox.vue
Normal file
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div class="search">
|
||||
<i class="icon ion-ios-search-strong"></i>
|
||||
<input class="input" type="email" v-bind:placeholder="$t('CHAT_LIST.SEARCH.INPUT')">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
};
|
||||
</script>
|
24
app/javascript/dashboard/components/widgets/StatusBar.vue
Normal file
24
app/javascript/dashboard/components/widgets/StatusBar.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="status-bar" :class="type">
|
||||
<p class="message">{{message}}</p>
|
||||
<router-link
|
||||
:to="buttonRoute"
|
||||
class="button small warning nice"
|
||||
v-if="showButton"
|
||||
>
|
||||
{{buttonText}}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: String,
|
||||
buttonRoute: Object,
|
||||
buttonText: String,
|
||||
showButton: Boolean,
|
||||
type: String, // Danger, Info, Success, Warning
|
||||
},
|
||||
};
|
||||
</script>
|
29
app/javascript/dashboard/components/widgets/Thumbnail.vue
Normal file
29
app/javascript/dashboard/components/widgets/Thumbnail.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="user-thumbnail-box" v-bind:style="{ height: size, width: size }">
|
||||
<img v-bind:src="src" class="user-thumbnail">
|
||||
<img class="source-badge" src="~dashboard/assets/images/fb-badge.png" v-if="badge === 'Facebook'">
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* Thumbnail Component
|
||||
* Src - source for round image
|
||||
* Size - Size of the thumbnail
|
||||
* Badge - Chat source indication { fb / telegram }
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '40px',
|
||||
},
|
||||
badge: {
|
||||
type: String,
|
||||
default: 'fb',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
import { Bar } from 'vue-chartjs';
|
||||
|
||||
const fontFamily =
|
||||
'-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif';
|
||||
|
||||
export default Bar.extend({
|
||||
props: ['collection'],
|
||||
mounted() {
|
||||
this.renderChart(this.collection, {
|
||||
// responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
labels: {
|
||||
fontFamily,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.9,
|
||||
ticks: {
|
||||
fontFamily,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
fontFamily,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<ul class="vertical dropdown menu canned" id="canned-list" v-bind:style="{ top: getTopPadding() + 'rem'}">
|
||||
<li
|
||||
v-for="(item, index) in cannedMessages"
|
||||
:id="`canned-${index}`"
|
||||
:class="{'active': index === selectedIndex}"
|
||||
v-on:click="onListItemSelection(index)"
|
||||
v-on:mouseover="onHover(index)"
|
||||
>
|
||||
<a><strong>{{item.short_code}}</strong> - {{item.content}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: ['onKeyenter', 'onClick'],
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
cannedMessages: 'getCannedResponses',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
/* eslint-disable no-confusing-arrow */
|
||||
document.addEventListener('keydown', this.keyListener);
|
||||
},
|
||||
methods: {
|
||||
getTopPadding() {
|
||||
if (this.cannedMessages.length <= 4) {
|
||||
return -this.cannedMessages.length * 3.5;
|
||||
}
|
||||
return -14;
|
||||
},
|
||||
isUp(e) {
|
||||
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
|
||||
},
|
||||
isDown(e) {
|
||||
return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
|
||||
},
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
keyListener(e) {
|
||||
if (this.isUp(e)) {
|
||||
if (!this.selectedIndex) {
|
||||
this.selectedIndex = this.cannedMessages.length - 1;
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
}
|
||||
if (this.isDown(e)) {
|
||||
if (this.selectedIndex === this.cannedMessages.length - 1) {
|
||||
this.selectedIndex = 0;
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
}
|
||||
if (this.isEnter(e)) {
|
||||
this.onKeyenter(this.cannedMessages[this.selectedIndex].content);
|
||||
}
|
||||
this.$el.querySelector('#canned-list').scrollTop = 34 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onListItemSelection(index) {
|
||||
this.selectedIndex = index;
|
||||
this.onClick(this.cannedMessages[this.selectedIndex].content);
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.keyListener);
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
<template>
|
||||
<select class="status--filter" v-model="activeIndex" @change="onTabChange()">
|
||||
<option v-for="(item, index) in $t('CHAT_LIST.CHAT_STAUTUS_ITEMS')" :value="item['VALUE']">{{item["TEXT"]}}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
activeIndex: 0,
|
||||
}),
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
onTabChange() {
|
||||
this.$store.dispatch('setChatFilter', this.activeIndex);
|
||||
this.$emit('statusFilterChange', this.activeIndex);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<li :class="alignBubble" v-if="data.attachment || data.content">
|
||||
<div :class="wrapClass">
|
||||
<p
|
||||
:class="{ bubble: isBubble, 'is-private': isPrivate }"
|
||||
v-tooltip.top-start="sentByMessage"
|
||||
>
|
||||
<bubble-image
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
v-if="data.attachment && data.attachment.file_type==='image'"
|
||||
/>
|
||||
<bubble-audio
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
v-if="data.attachment && data.attachment.file_type==='audio'"
|
||||
/>
|
||||
<bubble-map
|
||||
:lat="data.attachment.coordinates_lat"
|
||||
:lng="data.attachment.coordinates_long"
|
||||
:label="data.attachment.fallback_title"
|
||||
:readable-time="readableTime"
|
||||
v-if="data.attachment && data.attachment.file_type==='location'"
|
||||
/>
|
||||
<i class="icon ion-person" v-if="data.message_type === 2"></i>
|
||||
<bubble-text v-if="data.content" :message="message" :readable-time="readableTime"/>
|
||||
<i class="icon ion-android-lock" v-if="isPrivate" v-tooltip.top-start="toolTipMessage" @mouseenter="isHovered = true" @mouseleave="isHovered = false"></i>
|
||||
</p>
|
||||
</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"> -->
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint-disable no-named-as-default */
|
||||
import getEmojiSVG from '../emoji/utils';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleMap from './bubble/Map';
|
||||
import BubbleAudio from './bubble/Audio';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleText,
|
||||
BubbleImage,
|
||||
BubbleMap,
|
||||
BubbleAudio,
|
||||
},
|
||||
props: ['data'],
|
||||
mixins: [timeMixin],
|
||||
data() {
|
||||
return {
|
||||
isHovered: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
const linkifiedMessage = this.linkify(this.data.content);
|
||||
return linkifiedMessage.replace(/\n/g, '<br>');
|
||||
},
|
||||
alignBubble() {
|
||||
return this.data.message_type === 1 ? 'right' : 'left';
|
||||
},
|
||||
readableTime() {
|
||||
return this.messageStamp(this.data.created_at);
|
||||
},
|
||||
isBubble() {
|
||||
return this.data.message_type === 1 || this.data.message_type === 0;
|
||||
},
|
||||
isPrivate() {
|
||||
return this.data.private;
|
||||
},
|
||||
toolTipMessage() {
|
||||
return this.data.private ? { content: this.$t('CONVERSATION.VISIBLE_TO_AGENTS'), classes: 'top' } : false;
|
||||
},
|
||||
sentByMessage() {
|
||||
return this.data.message_type === 1 && !this.isHovered && this.data.sender !== undefined ?
|
||||
{ content: `Sent by: ${this.data.sender.name}`, classes: 'top' } : false;
|
||||
},
|
||||
wrapClass() {
|
||||
return {
|
||||
wrap: this.isBubble,
|
||||
'activity-wrap': !this.isBubble,
|
||||
};
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
getEmojiSVG,
|
||||
linkify(text) {
|
||||
if (!text) return text;
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
return text.replace(urlRegex, url => `<a href="${url}" target="_blank">${url}</a>`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,246 @@
|
|||
<template>
|
||||
<div class="medium-8 columns conversation-wrap">
|
||||
<div v-if="currentChat.id !== null" class="view-box columns">
|
||||
<conversation-header :chat="currentChat" />
|
||||
<ul class="conversation-panel">
|
||||
<transition name="slide-up">
|
||||
<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 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint no-extra-boolean-cast: 0 */
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ConversationHeader from './ConversationHeader';
|
||||
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 {
|
||||
components: {
|
||||
ConversationHeader,
|
||||
Conversation,
|
||||
ReplyBox,
|
||||
},
|
||||
|
||||
mixins: [conversationMixin, adminMixin],
|
||||
|
||||
props: {
|
||||
inboxId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoadingPrevious: true,
|
||||
heightBeforeLoad: null,
|
||||
conversationPanel: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
allConversations: 'getAllConversations',
|
||||
inboxesList: 'getInboxesList',
|
||||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
getUnreadCount: 'getUnreadCount',
|
||||
fetchingInboxes: 'getInboxLoadingStatus',
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
}),
|
||||
// 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: {
|
||||
focusLastMessage() {
|
||||
setTimeout(() => {
|
||||
this.attachListner();
|
||||
}, 0);
|
||||
},
|
||||
|
||||
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', {
|
||||
id: 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>
|
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div
|
||||
class="conversation"
|
||||
:class="{ active: isActiveChat, 'unread-chat': hasUnread }"
|
||||
@click="cardClick(chat)"
|
||||
>
|
||||
<Thumbnail
|
||||
:src="chat.meta.sender.thumbnail"
|
||||
:badge="chat.meta.sender.channel"
|
||||
class="columns"
|
||||
/>
|
||||
<div class="conversation--details columns">
|
||||
<h4 class="conversation--user">
|
||||
{{ chat.meta.sender.name }}
|
||||
<span
|
||||
v-if="isInboxNameVisible"
|
||||
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
||||
class="label"
|
||||
>
|
||||
{{ inboxName(chat.inbox_id) }}
|
||||
</span>
|
||||
</h4>
|
||||
<p
|
||||
class="conversation--message"
|
||||
v-html="extractMessageText(lastMessage(chat))"
|
||||
></p>
|
||||
|
||||
<div class="conversation--meta">
|
||||
<span class="timestamp">
|
||||
{{ dynamicTime(lastMessage(chat).created_at) }}
|
||||
</span>
|
||||
<span class="unread">{{ getUnreadCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint no-extra-boolean-cast: 0 */
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import getEmojiSVG from '../emoji/utils';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [timeMixin, conversationMixin],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
inboxesList: 'getInboxesList',
|
||||
activeInbox: 'getSelectedInbox',
|
||||
}),
|
||||
|
||||
isActiveChat() {
|
||||
return this.currentChat.id === this.chat.id;
|
||||
},
|
||||
|
||||
getUnreadCount() {
|
||||
return this.unreadMessagesCount(this.chat);
|
||||
},
|
||||
|
||||
hasUnread() {
|
||||
return this.getUnreadCount > 0;
|
||||
},
|
||||
|
||||
isInboxNameVisible() {
|
||||
return !this.activeInbox;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
router.push({
|
||||
path: frontendURL(`conversations/${chat.id}`),
|
||||
});
|
||||
},
|
||||
extractMessageText(chatItem) {
|
||||
if (chatItem.content) {
|
||||
return chatItem.content;
|
||||
}
|
||||
let fileType = '';
|
||||
if (chatItem.attachment) {
|
||||
fileType = chatItem.attachment.file_type;
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
||||
return `
|
||||
<i class="${this.$t(`${key}.ICON`)}"></i>
|
||||
${this.$t(`${key}.CONTENT`)}
|
||||
`;
|
||||
},
|
||||
getEmojiSVG,
|
||||
inboxName(inboxId) {
|
||||
const [stateInbox] = this.inboxesList.filter(
|
||||
inbox => inbox.channel_id === inboxId
|
||||
);
|
||||
return !stateInbox ? '' : stateInbox.label;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="conv-header">
|
||||
<div class="user">
|
||||
<Thumbnail
|
||||
:src="chat.meta.sender.thumbnail"
|
||||
size="40px"
|
||||
:badge="chat.meta.sender.channel"
|
||||
/>
|
||||
<h3 class="user--name">{{chat.meta.sender.name}}</h3>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="multiselect-box ion-headphone">
|
||||
<multiselect
|
||||
v-model="currentChat.meta.assignee"
|
||||
:options="agentList"
|
||||
label="name"
|
||||
@select="assignAgent"
|
||||
:allow-empty="true"
|
||||
deselect-label="Remove"
|
||||
placeholder="Select Agent"
|
||||
selected-label=''
|
||||
select-label="Assign"
|
||||
track-by="id"
|
||||
@remove="removeAgent"
|
||||
/>
|
||||
</div>
|
||||
<ResolveButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
<script>
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
/* eslint no-shadow: 0 */
|
||||
/* global bus */
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import ResolveButton from '../../buttons/ResolveButton';
|
||||
import EmojiInput from '../emoji/EmojiInput';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'chat',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'getVerifiedAgents',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
agentList() {
|
||||
return [
|
||||
{
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
},
|
||||
...this.agents,
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
assignAgent(agent) {
|
||||
this.$store.dispatch('assignAgent', [this.currentChat.id, agent.id]).then((response) => {
|
||||
console.log('assignAgent', response);
|
||||
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
},
|
||||
|
||||
removeAgent(agent) {
|
||||
console.log(agent.email);
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
Thumbnail,
|
||||
ResolveButton,
|
||||
EmojiInput,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<div class="reply-box">
|
||||
<div class="reply-box__top" :class="{ 'is-private': private }">
|
||||
<canned-response
|
||||
v-on-clickaway="hideCannedResponse"
|
||||
data-dropdown-menu
|
||||
:on-keyenter="replaceText"
|
||||
:on-click="replaceText"
|
||||
v-if="showCannedModal"
|
||||
/>
|
||||
<emoji-input v-on-clickaway="hideEmojiPicker" :on-click="emojiOnClick" v-if="showEmojiPicker"/>
|
||||
<textarea
|
||||
rows="1"
|
||||
v-model="message"
|
||||
class="input"
|
||||
type="text"
|
||||
@click="onClick()"
|
||||
@blur="onBlur()"
|
||||
v-bind:placeholder="$t(messagePlaceHolder())"
|
||||
ref="messageInput"
|
||||
/>
|
||||
<i class="icon ion-happy-outline" :class="{ active: showEmojiPicker}" @click="toggleEmojiPicker()"></i>
|
||||
</div>
|
||||
|
||||
<div class="reply-box__bottom" >
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title" v-bind:class="{ 'is-active': !private }">
|
||||
<a href="#" @click="makeReply" >Reply</a>
|
||||
</li>
|
||||
<li class="tabs-title is-private" v-bind:class="{ 'is-active': private }">
|
||||
<a href="#" @click="makePrivate">Private Note</a>
|
||||
</li>
|
||||
<li class="tabs-title message-length" v-if="message.length">
|
||||
<a :class="{ 'message-error': message.length > 620 }">{{ message.length }} / 640</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
type="button"
|
||||
class="button send-button"
|
||||
:disabled="disableButton()"
|
||||
v-bind:class="{ 'disabled': message.length === 0 || message.length > 640,
|
||||
'warning': private }"
|
||||
>
|
||||
{{ private ? $t('CONVERSATION.REPLYBOX.CREATE') : $t('CONVERSATION.REPLYBOX.SEND') }}
|
||||
<i class="icon" :class="{ 'ion-android-send': !private, 'ion-android-lock': private }"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import emojione from 'emojione';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import EmojiInput from '../emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
message: '',
|
||||
private: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedModal: false,
|
||||
};
|
||||
},
|
||||
computed: mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
components: {
|
||||
EmojiInput,
|
||||
CannedResponse,
|
||||
},
|
||||
mounted() {
|
||||
/* eslint-disable no-confusing-arrow */
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (this.isEscape(e)) {
|
||||
this.hideEmojiPicker();
|
||||
this.hideCannedResponse();
|
||||
}
|
||||
if (this.isEnter(e)) {
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
watch: {
|
||||
message(val) {
|
||||
if (this.private) {
|
||||
return;
|
||||
}
|
||||
const isSlashCommand = val[0] === '/';
|
||||
const hasNextWord = val.indexOf(' ') > -1;
|
||||
const isShortCodeActive = isSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.showCannedModal = true;
|
||||
if (val.length > 1) {
|
||||
const searchKey = val.substr(1, val.length);
|
||||
this.$store.dispatch('searchCannedResponse', {
|
||||
searchKey,
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('fetchCannedResponse');
|
||||
}
|
||||
} else {
|
||||
this.showCannedModal = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
isEscape(e) {
|
||||
return e.keyCode === 27; // ESCAPE
|
||||
},
|
||||
sendMessage() {
|
||||
const messageHasOnlyNewLines = !this.message.replace(/\n/g, '').length;
|
||||
if (messageHasOnlyNewLines) {
|
||||
return;
|
||||
}
|
||||
const messageAction = this.private ? 'addPrivateNote' : 'sendMessage';
|
||||
if (this.message.length !== 0 && !this.showCannedModal) {
|
||||
this.$store.dispatch(messageAction, [this.currentChat.id, this.message]).then(() => {
|
||||
this.$emit('scrollToMessage');
|
||||
});
|
||||
this.clearMessage();
|
||||
this.hideEmojiPicker();
|
||||
}
|
||||
},
|
||||
replaceText(message) {
|
||||
setTimeout(() => {
|
||||
this.message = message;
|
||||
}, 200);
|
||||
},
|
||||
makePrivate() {
|
||||
this.private = true;
|
||||
this.$refs.messageInput.focus();
|
||||
},
|
||||
makeReply() {
|
||||
this.private = false;
|
||||
this.$refs.messageInput.focus();
|
||||
},
|
||||
emojiOnClick(emoji) {
|
||||
this.message = emojione.shortnameToUnicode(`${this.message}${emoji.shortname} `);
|
||||
},
|
||||
clearMessage() {
|
||||
this.message = '';
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
if (this.showEmojiPicker) {
|
||||
this.toggleEmojiPicker();
|
||||
}
|
||||
},
|
||||
hideCannedResponse() {
|
||||
this.showCannedModal = false;
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
this.toggleTyping('off');
|
||||
},
|
||||
onClick() {
|
||||
this.markSeen();
|
||||
this.toggleTyping('on');
|
||||
},
|
||||
markSeen() {
|
||||
this.$store.dispatch('markSeen', {
|
||||
inboxId: this.currentChat.inbox_id,
|
||||
senderId: this.currentChat.meta.sender.id,
|
||||
});
|
||||
},
|
||||
|
||||
toggleTyping(flag) {
|
||||
this.$store.dispatch('toggleTyping', {
|
||||
flag,
|
||||
inboxId: this.currentChat.inbox_id,
|
||||
senderId: this.currentChat.meta.sender.id,
|
||||
});
|
||||
},
|
||||
disableButton() {
|
||||
const messageHasOnlyNewLines = !this.message.replace(/\n/g, '').length;
|
||||
return this.message.length === 0 || this.message.length > 640 || messageHasOnlyNewLines;
|
||||
},
|
||||
|
||||
messagePlaceHolder() {
|
||||
const placeHolder = this.private ? 'CONVERSATION.FOOTER.PRIVATE_MSG_INPUT' : 'CONVERSATION.FOOTER.MSG_INPUT';
|
||||
return placeHolder;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.send-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="audio message-text__wrap">
|
||||
<a-player
|
||||
:music="playerOptions"
|
||||
mode="order"
|
||||
/>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import APlayer from 'vue-aplayer';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
APlayer,
|
||||
},
|
||||
props: [
|
||||
'url',
|
||||
'readableTime',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
musicObj: {
|
||||
title: ' ',
|
||||
author: ' ',
|
||||
autoplay: false,
|
||||
narrow: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
playerOptions() {
|
||||
return {
|
||||
...this.musicObj,
|
||||
url: this.url,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="image message-text__wrap">
|
||||
<img
|
||||
:src="url"
|
||||
v-on:click="onClick"
|
||||
/>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<img
|
||||
:src="url"
|
||||
class="modal-image"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'url',
|
||||
'readableTime',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
onClick() {
|
||||
this.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="map message-text__wrap">
|
||||
<img
|
||||
:src="locUrl"
|
||||
/>
|
||||
<span class="locname">{{label || ' '}}</span>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'lat',
|
||||
'lng',
|
||||
'label',
|
||||
'readableTime',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
accessToken: 'pk.eyJ1IjoiY2hhdHdvb3QiLCJhIjoiY2oyazVsM3d0MDBmYjJxbmkyYXlwY3hzZyJ9.uWUdfItb0sSZQ4nfwlmuPg',
|
||||
zoomLevel: 14,
|
||||
mapType: 'mapbox.streets',
|
||||
apiEndPoint: 'https://api.mapbox.com/v4/',
|
||||
h: 100,
|
||||
w: 150,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locUrl() {
|
||||
const { apiEndPoint, mapType, lat, lng, zoomLevel, h, w, accessToken } = this;
|
||||
return `${apiEndPoint}${mapType}/${lng},${lat},${zoomLevel}/${w}x${h}.png?access_token=${accessToken}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<span class="message-text__wrap">
|
||||
<span v-html="message" class="message-text"></span>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'message',
|
||||
'readableTime',
|
||||
],
|
||||
};
|
||||
</script>
|
103
app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue
Normal file
103
app/javascript/dashboard/components/widgets/emoji/EmojiInput.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div role="dialog" class="emoji-dialog">
|
||||
<header class="emoji-dialog-header" role="menu">
|
||||
<ul>
|
||||
<li
|
||||
v-bind:class="{ 'active': selectedKey === category.key }"
|
||||
v-for="category in categoryList"
|
||||
@click="changeCategory(category)"
|
||||
>
|
||||
<div
|
||||
@click="changeCategory(category)"
|
||||
role="menuitem"
|
||||
class="emojione"
|
||||
v-html="getEmojiUnicode(`:${category.emoji}:`)"
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div class="emoji-row">
|
||||
<h5 class="emoji-category-title">{{selectedKey}}</h5>
|
||||
<div
|
||||
v-for="(emoji, key) in selectedEmojis"
|
||||
role="menuitem"
|
||||
:class="`emojione`"
|
||||
v-html="getEmojiUnicode(emoji[emoji.length - 1].shortname)"
|
||||
v-if="filterEmoji(emoji[emoji.length - 1].shortname)"
|
||||
track-by="$index"
|
||||
@click="onClick(emoji[emoji.length - 1])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import strategy from 'emojione/emoji.json';
|
||||
import categoryList from './categories';
|
||||
import { getEmojiUnicode } from './utils';
|
||||
|
||||
export default {
|
||||
props: ['onClick'],
|
||||
data() {
|
||||
return {
|
||||
selectedKey: 'people',
|
||||
categoryList,
|
||||
selectedEmojis: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
emojis() {
|
||||
const emojiArr = {};
|
||||
|
||||
// categorise and nest emoji
|
||||
// sort ensures that modifiers appear unmodified keys
|
||||
const keys = Object.keys(strategy);
|
||||
for (const key of keys) {
|
||||
const value = strategy[key];
|
||||
|
||||
// skip unknown categoryList
|
||||
if (value.category !== 'modifier') {
|
||||
if (!emojiArr[value.category]) emojiArr[value.category] = {};
|
||||
const match = key.match(/(.*?)_tone(.*?)$/);
|
||||
|
||||
if (match) {
|
||||
// this check is to stop the plugin from failing in the case that the
|
||||
// emoji strategy miscategorizes tones - which was the case here:
|
||||
const unmodifiedEmojiExists = !!emojiArr[value.category][match[1]];
|
||||
if (unmodifiedEmojiExists) {
|
||||
emojiArr[value.category][match[1]][match[2]] = value;
|
||||
}
|
||||
} else {
|
||||
emojiArr[value.category][key] = [value];
|
||||
}
|
||||
}
|
||||
}
|
||||
return emojiArr;
|
||||
},
|
||||
},
|
||||
// On mount render initial emoji
|
||||
mounted() {
|
||||
this.getInitialEmoji();
|
||||
},
|
||||
methods: {
|
||||
|
||||
// Change category and associated emojis
|
||||
changeCategory(category) {
|
||||
this.selectedKey = category.key;
|
||||
this.selectedEmojis = this.emojis[this.selectedKey];
|
||||
},
|
||||
|
||||
// Filter non-existant or irregular unicode characters
|
||||
filterEmoji(shortName) {
|
||||
return shortName !== ':relaxed:' && shortName !== ':frowning2:';
|
||||
},
|
||||
// Get inital emojis
|
||||
getInitialEmoji() {
|
||||
this.selectedEmojis = this.emojis.people;
|
||||
},
|
||||
getEmojiUnicode,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
export default [ // eslint-disable-line
|
||||
{
|
||||
key: 'people',
|
||||
title: 'People',
|
||||
emoji: 'smile',
|
||||
},
|
||||
{
|
||||
key: 'nature',
|
||||
title: 'Nature',
|
||||
emoji: 'hamster',
|
||||
},
|
||||
{
|
||||
key: 'food',
|
||||
title: 'Food & Drink',
|
||||
emoji: 'pizza',
|
||||
},
|
||||
{
|
||||
key: 'activity',
|
||||
title: 'Activity',
|
||||
emoji: 'soccer',
|
||||
},
|
||||
{
|
||||
key: 'travel',
|
||||
title: 'Travel & Places',
|
||||
emoji: 'earth_americas',
|
||||
},
|
||||
{
|
||||
key: 'objects',
|
||||
title: 'Objects',
|
||||
emoji: 'bulb',
|
||||
},
|
||||
{
|
||||
key: 'symbols',
|
||||
title: 'Symbols',
|
||||
emoji: 'clock9',
|
||||
},
|
||||
{
|
||||
key: 'flags',
|
||||
title: 'Flags',
|
||||
emoji: 'flag_gb',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,9 @@
|
|||
import emojione from 'emojione';
|
||||
/* eslint-disable */
|
||||
export default function (value, method = 'shortnameToImage') {
|
||||
return emojione[method](value);
|
||||
}
|
||||
|
||||
export function getEmojiUnicode(value) {
|
||||
return emojione.shortnameToUnicode(value);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue