feat: notification center (#1612)
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
parent
e75916d562
commit
c087e75808
23 changed files with 811 additions and 12 deletions
|
@ -1,6 +1,7 @@
|
||||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||||
protect_from_forgery with: :null_session
|
RESULTS_PER_PAGE = 15
|
||||||
|
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
before_action :fetch_notification, only: [:update]
|
before_action :fetch_notification, only: [:update]
|
||||||
before_action :set_primary_actor, only: [:read_all]
|
before_action :set_primary_actor, only: [:read_all]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
@ -8,17 +9,18 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||||
def index
|
def index
|
||||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||||
@count = notifications.count
|
@count = notifications.count
|
||||||
@notifications = notifications.page @current_page
|
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_all
|
def read_all
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
if @primary_actor
|
if @primary_actor
|
||||||
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
|
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
|
||||||
.update(read_at: DateTime.now.utc)
|
.update_all(read_at: DateTime.now.utc)
|
||||||
else
|
else
|
||||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
|
current_user.notifications.where(account_id: current_account.id, read_at: nil).update_all(read_at: DateTime.now.utc)
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
33
app/javascript/dashboard/api/notifications.js
Normal file
33
app/javascript/dashboard/api/notifications.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class NotificationsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('notifications', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(page) {
|
||||||
|
return axios.get(`${this.url}?page=${page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotifications(contactId) {
|
||||||
|
return axios.get(`${this.url}/${contactId}/notifications`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnreadCount() {
|
||||||
|
return axios.get(`${this.url}/unread_count`);
|
||||||
|
}
|
||||||
|
|
||||||
|
read(primaryActorType, primaryActorId) {
|
||||||
|
return axios.post(`${this.url}/read_all`, {
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readAll() {
|
||||||
|
return axios.post(`${this.url}/read_all`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NotificationsAPI();
|
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import notifications from '../notifications';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#NotificationAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(notifications).toBeInstanceOf(ApiClient);
|
||||||
|
expect(notifications).toHaveProperty('get');
|
||||||
|
expect(notifications).toHaveProperty('getNotifications');
|
||||||
|
expect(notifications).toHaveProperty('getUnreadCount');
|
||||||
|
expect(notifications).toHaveProperty('read');
|
||||||
|
expect(notifications).toHaveProperty('readAll');
|
||||||
|
});
|
||||||
|
});
|
|
@ -106,13 +106,13 @@
|
||||||
font-size: $font-size-medium;
|
font-size: $font-size-medium;
|
||||||
margin-top: $space-medium;
|
margin-top: $space-medium;
|
||||||
|
|
||||||
>span {
|
> span {
|
||||||
margin-left: $space-one;
|
margin-left: $space-one;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-title+ul>li>a {
|
.menu-title + ul > li > a {
|
||||||
@include padding($space-micro null);
|
@include padding($space-micro null);
|
||||||
color: $medium-gray;
|
color: $medium-gray;
|
||||||
line-height: $global-lineheight;
|
line-height: $global-lineheight;
|
||||||
|
@ -152,6 +152,26 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
font-size: var(--font-size-big);
|
||||||
|
margin-bottom: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-top: auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.unread-badge {
|
||||||
|
background: var(--r-300);
|
||||||
|
border-radius: var(--space-small);
|
||||||
|
color: var(--white);
|
||||||
|
font-size: var(--font-size-micro);
|
||||||
|
font-weight: var(--font-weight-black);
|
||||||
|
left: var(--space-slab);
|
||||||
|
padding: 0 var(--space-smaller);
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hamburger--menu {
|
.hamburger--menu {
|
||||||
|
|
|
@ -73,6 +73,13 @@
|
||||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
class="notifications icon ion-ios-bell"
|
||||||
|
@click.stop="showNotification"
|
||||||
|
>
|
||||||
|
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="current-user--options icon ion-android-more-vertical" />
|
<span class="current-user--options icon ion-android-more-vertical" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,7 +141,7 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer medium-12 columns">
|
<div class="modal-footer medium-12 columns">
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:disabled="
|
:disabled="
|
||||||
|
@ -206,6 +213,7 @@ export default {
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
uiFlags: 'agents/getUIFlags',
|
uiFlags: 'agents/getUIFlags',
|
||||||
accountLabels: 'labels/getLabelsOnSidebar',
|
accountLabels: 'labels/getLabelsOnSidebar',
|
||||||
|
notificationMetadata: 'notifications/getMeta',
|
||||||
}),
|
}),
|
||||||
currentUserAvailableName() {
|
currentUserAvailableName() {
|
||||||
return this.currentUser.name;
|
return this.currentUser.name;
|
||||||
|
@ -284,10 +292,20 @@ export default {
|
||||||
dashboardPath() {
|
dashboardPath() {
|
||||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||||
},
|
},
|
||||||
|
unreadCount() {
|
||||||
|
if (!this.notificationMetadata.unreadCount) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.notificationMetadata.unreadCount < 100
|
||||||
|
? this.notificationMetadata.unreadCount
|
||||||
|
: '99+';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('labels/get');
|
this.$store.dispatch('labels/get');
|
||||||
this.$store.dispatch('inboxes/get');
|
this.$store.dispatch('inboxes/get');
|
||||||
|
this.$store.dispatch('notifications/unReadCount');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterMenuItemsByRole(menuItems) {
|
filterMenuItemsByRole(menuItems) {
|
||||||
|
@ -307,6 +325,9 @@ export default {
|
||||||
showOptions() {
|
showOptions() {
|
||||||
this.showOptionsMenu = !this.showOptionsMenu;
|
this.showOptionsMenu = !this.showOptionsMenu;
|
||||||
},
|
},
|
||||||
|
showNotification() {
|
||||||
|
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
||||||
|
},
|
||||||
changeAccount() {
|
changeAccount() {
|
||||||
this.showAccountModal = true;
|
this.showAccountModal = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,11 +8,13 @@ export const getSidebarItems = accountId => ({
|
||||||
'inbox_conversation',
|
'inbox_conversation',
|
||||||
'conversation_through_inbox',
|
'conversation_through_inbox',
|
||||||
'contacts_dashboard',
|
'contacts_dashboard',
|
||||||
|
'notifications_dashboard',
|
||||||
'settings_account_reports',
|
'settings_account_reports',
|
||||||
'profile_settings',
|
'profile_settings',
|
||||||
'profile_settings_index',
|
'profile_settings_index',
|
||||||
'label_conversations',
|
'label_conversations',
|
||||||
'conversations_through_label',
|
'conversations_through_label',
|
||||||
|
'notifications_index',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: {
|
||||||
assignedToMe: {
|
assignedToMe: {
|
||||||
|
@ -31,6 +33,13 @@ export const getSidebarItems = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
toStateName: 'contacts_dashboard',
|
toStateName: 'contacts_dashboard',
|
||||||
},
|
},
|
||||||
|
notifications: {
|
||||||
|
icon: 'ion-ios-bell',
|
||||||
|
label: 'NOTIFICATIONS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||||
|
toStateName: 'notifications_dashboard',
|
||||||
|
},
|
||||||
report: {
|
report: {
|
||||||
icon: 'ion-arrow-graph-up-right',
|
icon: 'ion-arrow-graph-up-right',
|
||||||
label: 'REPORTS',
|
label: 'REPORTS',
|
||||||
|
|
|
@ -51,5 +51,24 @@
|
||||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||||
"SELECT_ONE": "Select one"
|
"SELECT_ONE": "Select one"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"NOTIFICATIONS_PAGE": {
|
||||||
|
"HEADER": "Notifications",
|
||||||
|
"MARK_ALL_DONE": "Mark All Done",
|
||||||
|
"LIST": {
|
||||||
|
"LOADING_MESSAGE": "Loading notifications...",
|
||||||
|
"404": "No Notifications",
|
||||||
|
"TABLE_HEADER": [
|
||||||
|
"Name",
|
||||||
|
"Phone Number",
|
||||||
|
"Conversations",
|
||||||
|
"Last Contacted"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TYPE_LABEL": {
|
||||||
|
"conversation_creation": "New conversation",
|
||||||
|
"conversation_assignment": "Conversation Assigned",
|
||||||
|
"assigned_conversation_new_message": "New Message"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@
|
||||||
"HOME": "Home",
|
"HOME": "Home",
|
||||||
"AGENTS": "Agents",
|
"AGENTS": "Agents",
|
||||||
"INBOXES": "Inboxes",
|
"INBOXES": "Inboxes",
|
||||||
|
"NOTIFICATIONS": "Notifications",
|
||||||
"CANNED_RESPONSES": "Canned Responses",
|
"CANNED_RESPONSES": "Canned Responses",
|
||||||
"INTEGRATIONS": "Integrations",
|
"INTEGRATIONS": "Integrations",
|
||||||
"ACCOUNT_SETTINGS": "Account Settings",
|
"ACCOUNT_SETTINGS": "Account Settings",
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
:on-click-contact="openContactInfoPanel"
|
:on-click-contact="openContactInfoPanel"
|
||||||
:active-contact-id="selectedContactId"
|
:active-contact-id="selectedContactId"
|
||||||
/>
|
/>
|
||||||
<contacts-footer
|
<table-footer
|
||||||
:on-page-change="onPageChange"
|
:on-page-change="onPageChange"
|
||||||
:current-page="Number(meta.currentPage)"
|
:current-page="Number(meta.currentPage)"
|
||||||
:total-count="meta.count"
|
:total-count="meta.count"
|
||||||
|
@ -34,13 +34,13 @@ import { mapGetters } from 'vuex';
|
||||||
import ContactsHeader from './Header';
|
import ContactsHeader from './Header';
|
||||||
import ContactsTable from './ContactsTable';
|
import ContactsTable from './ContactsTable';
|
||||||
import ContactInfoPanel from './ContactInfoPanel';
|
import ContactInfoPanel from './ContactInfoPanel';
|
||||||
import ContactsFooter from './Footer';
|
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContactsHeader,
|
ContactsHeader,
|
||||||
ContactsTable,
|
ContactsTable,
|
||||||
ContactsFooter,
|
TableFooter,
|
||||||
ContactInfoPanel,
|
ContactInfoPanel,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import AppContainer from './Dashboard';
|
||||||
import settings from './settings/settings.routes';
|
import settings from './settings/settings.routes';
|
||||||
import conversation from './conversation/conversation.routes';
|
import conversation from './conversation/conversation.routes';
|
||||||
import { routes as contactRoutes } from './contacts/routes';
|
import { routes as contactRoutes } from './contacts/routes';
|
||||||
|
import { routes as notificationRoutes } from './notifications/routes';
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -9,7 +10,12 @@ export default {
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:account_id'),
|
path: frontendURL('accounts/:account_id'),
|
||||||
component: AppContainer,
|
component: AppContainer,
|
||||||
children: [...conversation.routes, ...settings.routes, ...contactRoutes],
|
children: [
|
||||||
|
...conversation.routes,
|
||||||
|
...settings.routes,
|
||||||
|
...contactRoutes,
|
||||||
|
...notificationRoutes,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<section class="notification--table-wrap">
|
||||||
|
<woot-submit-button
|
||||||
|
v-if="notificationMetadata.unreadCount"
|
||||||
|
class="button nice success button--fixed-right-top"
|
||||||
|
:button-text="$t('NOTIFICATIONS_PAGE.MARK_ALL_DONE')"
|
||||||
|
:loading="isUpdating"
|
||||||
|
@click="onMarkAllDoneClick"
|
||||||
|
>
|
||||||
|
</woot-submit-button>
|
||||||
|
|
||||||
|
<table class="woot-table notifications-table">
|
||||||
|
<tbody v-show="!isLoading">
|
||||||
|
<tr
|
||||||
|
v-for="notificationItem in notifications"
|
||||||
|
:key="notificationItem.id"
|
||||||
|
@click="() => onClickNotification(notificationItem)"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<div class="notification--thumbnail">
|
||||||
|
<thumbnail
|
||||||
|
:src="notificationItem.primary_actor.meta.sender.thumbnail"
|
||||||
|
size="36px"
|
||||||
|
:username="notificationItem.primary_actor.meta.sender.name"
|
||||||
|
:status="
|
||||||
|
notificationItem.primary_actor.meta.sender.availability_status
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 class="notification--name">
|
||||||
|
{{ `#${notificationItem.id}` }}
|
||||||
|
</h4>
|
||||||
|
<p class="notification--title">
|
||||||
|
{{ notificationItem.push_message_title }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="label">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ dynamicTime(notificationItem.created_at) }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
v-if="!notificationItem.read_at"
|
||||||
|
class="notification--unread-indicator"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<empty-state
|
||||||
|
v-if="showEmptyResult"
|
||||||
|
:title="$t('NOTIFICATIONS_PAGE.LIST.404')"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoading" class="notifications--loader">
|
||||||
|
<spinner />
|
||||||
|
<span>{{ $t('NOTIFICATIONS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||||
|
import timeMixin from '../../../../mixins/time';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
Spinner,
|
||||||
|
EmptyState,
|
||||||
|
},
|
||||||
|
mixins: [timeMixin],
|
||||||
|
props: {
|
||||||
|
notifications: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isUpdating: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
onClickNotification: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
onMarkAllDoneClick: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
notificationMetadata: 'notifications/getMeta',
|
||||||
|
}),
|
||||||
|
showEmptyResult() {
|
||||||
|
return !this.isLoading && this.notifications.length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
|
.notification--name {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--title {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--table-wrap {
|
||||||
|
@include scroll-on-hover;
|
||||||
|
flex: 1 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table {
|
||||||
|
> tbody {
|
||||||
|
> tr {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--b-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--b-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
> td {
|
||||||
|
&.conversation-count-item {
|
||||||
|
padding-left: var(--space-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.notification--thumbnail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.user-thumbnail-box {
|
||||||
|
margin-right: var(--space-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications--loader {
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-big);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--unread-indicator {
|
||||||
|
width: var(--space-one);
|
||||||
|
height: var(--space-one);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-woot);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="columns notification--page">
|
||||||
|
<div class="notification--content medium-12">
|
||||||
|
<notification-table
|
||||||
|
:notifications="records"
|
||||||
|
:is-loading="uiFlags.isFetching"
|
||||||
|
:is-updating="uiFlags.isUpdating"
|
||||||
|
:on-click-notification="openConversation"
|
||||||
|
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||||
|
/>
|
||||||
|
<table-footer
|
||||||
|
:on-page-change="onPageChange"
|
||||||
|
:current-page="Number(meta.currentPage)"
|
||||||
|
:total-count="meta.count"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||||
|
|
||||||
|
import NotificationTable from './NotificationTable';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NotificationTable,
|
||||||
|
TableFooter,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
meta: 'notifications/getMeta',
|
||||||
|
records: 'notifications/getNotifications',
|
||||||
|
uiFlags: 'notifications/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('notifications/get', { page: 1 });
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPageChange(page) {
|
||||||
|
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||||
|
this.$store.dispatch('notifications/get', { page });
|
||||||
|
},
|
||||||
|
openConversation(notification) {
|
||||||
|
const {
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
primary_actor: { id: conversationId },
|
||||||
|
} = notification;
|
||||||
|
|
||||||
|
this.$store.dispatch('notifications/read', {
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount: this.meta.unreadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$router.push(
|
||||||
|
`/app/accounts/${this.accountId}/conversations/${conversationId}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMarkAllDoneClick() {
|
||||||
|
this.$store.dispatch('notifications/readAll');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.notification--page {
|
||||||
|
background: var(--white);
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification--content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* eslint arrow-body-style: 0 */
|
||||||
|
import NotificationsView from './components/NotificationsView.vue';
|
||||||
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
import SettingsWrapper from '../settings/Wrapper';
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/notifications'),
|
||||||
|
component: SettingsWrapper,
|
||||||
|
props: {
|
||||||
|
headerTitle: 'NOTIFICATIONS_PAGE.HEADER',
|
||||||
|
icon: 'ion-ios-bell',
|
||||||
|
showNewButton: false,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'notifications_index',
|
||||||
|
component: NotificationsView,
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
|
@ -7,6 +7,7 @@ import auth from './modules/auth';
|
||||||
import cannedResponse from './modules/cannedResponse';
|
import cannedResponse from './modules/cannedResponse';
|
||||||
import contactConversations from './modules/contactConversations';
|
import contactConversations from './modules/contactConversations';
|
||||||
import contacts from './modules/contacts';
|
import contacts from './modules/contacts';
|
||||||
|
import notifications from './modules/notifications';
|
||||||
import conversationLabels from './modules/conversationLabels';
|
import conversationLabels from './modules/conversationLabels';
|
||||||
import conversationMetadata from './modules/conversationMetadata';
|
import conversationMetadata from './modules/conversationMetadata';
|
||||||
import conversationPage from './modules/conversationPage';
|
import conversationPage from './modules/conversationPage';
|
||||||
|
@ -32,6 +33,7 @@ export default new Vuex.Store({
|
||||||
cannedResponse,
|
cannedResponse,
|
||||||
contactConversations,
|
contactConversations,
|
||||||
contacts,
|
contacts,
|
||||||
|
notifications,
|
||||||
conversationLabels,
|
conversationLabels,
|
||||||
conversationMetadata,
|
conversationMetadata,
|
||||||
conversationPage,
|
conversationPage,
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import types from '../../mutation-types';
|
||||||
|
import NotificationsAPI from '../../../api/notifications';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async ({ commit }, { page = 1 } = {}) => {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { payload, meta },
|
||||||
|
},
|
||||||
|
} = await NotificationsAPI.get(page);
|
||||||
|
commit(types.CLEAR_NOTIFICATIONS);
|
||||||
|
commit(types.SET_NOTIFICATIONS, payload);
|
||||||
|
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unReadCount: async ({ commit } = {}) => {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true });
|
||||||
|
try {
|
||||||
|
const { data } = await NotificationsAPI.getUnreadCount();
|
||||||
|
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, data);
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
read: async (
|
||||||
|
{ commit },
|
||||||
|
{ primaryActorType, primaryActorId, unreadCount }
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await NotificationsAPI.read(primaryActorType, primaryActorId);
|
||||||
|
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, unreadCount - 1);
|
||||||
|
commit(types.UPDATE_NOTIFICATION, primaryActorId);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readAll: async ({ commit }) => {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true });
|
||||||
|
try {
|
||||||
|
await NotificationsAPI.readAll();
|
||||||
|
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, 0);
|
||||||
|
commit(types.UPDATE_ALL_NOTIFICATIONS);
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const getters = {
|
||||||
|
getNotifications($state) {
|
||||||
|
return Object.values($state.records).sort((n1, n2) => n2.id - n1.id);
|
||||||
|
},
|
||||||
|
getUIFlags($state) {
|
||||||
|
return $state.uiFlags;
|
||||||
|
},
|
||||||
|
getNotification: $state => id => {
|
||||||
|
const notification = $state.records[id];
|
||||||
|
return notification || {};
|
||||||
|
},
|
||||||
|
getMeta: $state => {
|
||||||
|
return $state.meta;
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { getters } from './getters';
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { mutations } from './mutations';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
meta: {
|
||||||
|
count: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
unReadCount: 0,
|
||||||
|
},
|
||||||
|
records: {},
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
isFetchingItem: false,
|
||||||
|
isUpdating: false,
|
||||||
|
isUpdatingUnreadCount: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -0,0 +1,48 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import types from '../../mutation-types';
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG]($state, data) {
|
||||||
|
$state.uiFlags = {
|
||||||
|
...$state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.CLEAR_NOTIFICATIONS]: $state => {
|
||||||
|
Vue.set($state, 'records', {});
|
||||||
|
},
|
||||||
|
[types.SET_NOTIFICATIONS_META]: ($state, data) => {
|
||||||
|
const {
|
||||||
|
count,
|
||||||
|
current_page: currentPage,
|
||||||
|
unread_count: unreadCount,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
Vue.set($state.meta, 'count', count);
|
||||||
|
Vue.set($state.meta, 'currentPage', currentPage);
|
||||||
|
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||||
|
},
|
||||||
|
[types.SET_NOTIFICATIONS_UNREAD_COUNT]: ($state, count) => {
|
||||||
|
Vue.set($state.meta, 'unreadCount', count);
|
||||||
|
},
|
||||||
|
[types.SET_NOTIFICATIONS]: ($state, data) => {
|
||||||
|
data.forEach(notification => {
|
||||||
|
Vue.set($state.records, notification.id, {
|
||||||
|
...($state.records[notification.id] || {}),
|
||||||
|
...notification,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[types.UPDATE_NOTIFICATION]: ($state, primaryActorId) => {
|
||||||
|
Object.values($state.records).forEach(item => {
|
||||||
|
if (item.primary_actor_id === primaryActorId) {
|
||||||
|
Vue.set($state.records[item.id], 'read_at', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[types.UPDATE_ALL_NOTIFICATIONS]: $state => {
|
||||||
|
Object.values($state.records).forEach(item => {
|
||||||
|
Vue.set($state.records[item.id], 'read_at', true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,93 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { actions } from '../../notifications/actions';
|
||||||
|
import types from '../../../mutation-types';
|
||||||
|
|
||||||
|
const commit = jest.fn();
|
||||||
|
global.axios = axios;
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
describe('#actions', () => {
|
||||||
|
describe('#get', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.get.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
payload: [{ id: 1 }],
|
||||||
|
meta: { count: 3, current_page: 1, unread_count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.CLEAR_NOTIFICATIONS],
|
||||||
|
[types.SET_NOTIFICATIONS, [{ id: 1 }]],
|
||||||
|
[
|
||||||
|
types.SET_NOTIFICATIONS_META,
|
||||||
|
{ count: 3, current_page: 1, unread_count: 2 },
|
||||||
|
],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#unReadCount', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.get.mockResolvedValue({ data: 1 });
|
||||||
|
await actions.unReadCount({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true }],
|
||||||
|
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 1],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await actions.unReadCount({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true }],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#read', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.post.mockResolvedValue({});
|
||||||
|
await actions.read({ commit }, { unreadCount: 2, primaryActorId: 1 });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 1],
|
||||||
|
[types.UPDATE_NOTIFICATION, 1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(actions.read({ commit })).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#readAll', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.post.mockResolvedValue({ data: 1 });
|
||||||
|
await actions.readAll({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true }],
|
||||||
|
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 0],
|
||||||
|
[types.UPDATE_ALL_NOTIFICATIONS],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(actions.readAll({ commit })).rejects.toThrow(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { getters } from '../../notifications/getters';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getNotifications', () => {
|
||||||
|
const state = {
|
||||||
|
records: {
|
||||||
|
1: { id: 1 },
|
||||||
|
2: { id: 2 },
|
||||||
|
3: { id: 3 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getNotifications(state)).toEqual([
|
||||||
|
{ id: 3 },
|
||||||
|
{ id: 2 },
|
||||||
|
{ id: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getUIFlags', () => {
|
||||||
|
const state = {
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUIFlags(state)).toEqual({
|
||||||
|
isFetching: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getNotification', () => {
|
||||||
|
const state = {
|
||||||
|
records: {
|
||||||
|
1: { id: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getNotification(state)(1)).toEqual({ id: 1 });
|
||||||
|
expect(getters.getNotification(state)(2)).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getMeta', () => {
|
||||||
|
const state = {
|
||||||
|
meta: { unreadCount: 1 },
|
||||||
|
};
|
||||||
|
expect(getters.getMeta(state)).toEqual({ unreadCount: 1 });
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,90 @@
|
||||||
|
import types from '../../../mutation-types';
|
||||||
|
import { mutations } from '../../notifications/mutations';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_NOTIFICATIONS_UI_FLAG', () => {
|
||||||
|
it('set notification ui flag', () => {
|
||||||
|
const state = { uiFlags: { isFetching: true } };
|
||||||
|
mutations[types.SET_NOTIFICATIONS_UI_FLAG](state, { isFetching: false });
|
||||||
|
expect(state.uiFlags).toEqual({ isFetching: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#CLEAR_NOTIFICATIONS', () => {
|
||||||
|
it('clear notifications', () => {
|
||||||
|
const state = { records: { 1: { id: 1 } } };
|
||||||
|
mutations[types.CLEAR_NOTIFICATIONS](state);
|
||||||
|
expect(state.records).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#SET_NOTIFICATIONS_META', () => {
|
||||||
|
it('set notifications meta data', () => {
|
||||||
|
const state = { meta: {} };
|
||||||
|
mutations[types.SET_NOTIFICATIONS_META](state, {
|
||||||
|
count: 3,
|
||||||
|
current_page: 1,
|
||||||
|
unread_count: 2,
|
||||||
|
});
|
||||||
|
expect(state.meta).toEqual({
|
||||||
|
count: 3,
|
||||||
|
currentPage: 1,
|
||||||
|
unreadCount: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#SET_NOTIFICATIONS_UNREAD_COUNT', () => {
|
||||||
|
it('set notifications unread count', () => {
|
||||||
|
const state = { meta: { unreadCount: 4 } };
|
||||||
|
mutations[types.SET_NOTIFICATIONS_UNREAD_COUNT](state, 3);
|
||||||
|
expect(state.meta).toEqual({ unreadCount: 3 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#SET_NOTIFICATIONS', () => {
|
||||||
|
it('set notifications ', () => {
|
||||||
|
const state = { records: {} };
|
||||||
|
mutations[types.SET_NOTIFICATIONS](state, [
|
||||||
|
{ id: 1 },
|
||||||
|
{ id: 2 },
|
||||||
|
{ id: 3 },
|
||||||
|
{ id: 4 },
|
||||||
|
]);
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: { id: 1 },
|
||||||
|
2: { id: 2 },
|
||||||
|
3: { id: 3 },
|
||||||
|
4: { id: 4 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('#UPDATE_NOTIFICATION', () => {
|
||||||
|
it('update notifications ', () => {
|
||||||
|
const state = {
|
||||||
|
records: {
|
||||||
|
1: { id: 1, primary_actor_id: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mutations[types.UPDATE_NOTIFICATION](state, 1);
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('#UPDATE_ALL_NOTIFICATIONS', () => {
|
||||||
|
it('update all notifications ', () => {
|
||||||
|
const state = {
|
||||||
|
records: {
|
||||||
|
1: { id: 1, primary_actor_id: 1 },
|
||||||
|
2: { id: 2, primary_actor_id: 2 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mutations[types.UPDATE_ALL_NOTIFICATIONS](state);
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||||
|
2: { id: 2, primary_actor_id: 2, read_at: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -98,6 +98,18 @@ export default {
|
||||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
SET_NOTIFICATIONS_META: 'SET_NOTIFICATIONS_META',
|
||||||
|
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
|
||||||
|
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
|
||||||
|
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
|
||||||
|
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
|
||||||
|
SET_NOTIFICATIONS_ITEM: 'SET_NOTIFICATIONS_ITEM',
|
||||||
|
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
||||||
|
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||||
|
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||||
|
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||||
|
|
||||||
// Contact Conversation
|
// Contact Conversation
|
||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||||
|
|
Loading…
Reference in a new issue