mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
chore: Update inbox view to perform better, added sidebar on inbox views (#12077)
# Pull Request Template ## Description This PR includes improvements to Inbox view: 1. **Update the route to `:type/:id`** Previously, we used `notification_id` in the route. This has now been changed to use a more generic structure like `conversation/:id`, with `type` set to `"conversation"`. This refactor allows future support for other types like `contact`, making the route structure more flexible. It also fixes a critical issue: when a notification is open and a new notification arrives for the same conversation, the conversation view used to close unexpectedly. This issue is now resolved. 2. **Migrate components from Options API to Composition API** Both `InboxList.vue` and `InboxView.vue` have been updated to use the Composition API with `<script setup>`. 3. **Auto-scroll inbox item into view when navigating** When navigating through `InboxItemHeader`, the corresponding inbox item now automatically scrolls into view and load more notifications ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -153,7 +153,7 @@ onBeforeMount(contextMenuActions.close);
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
class="flex flex-col w-full gap-2 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
class="flex flex-col w-full gap-1 p-3 transition-all duration-300 ease-in-out cursor-pointer"
|
||||||
@contextmenu="contextMenuActions.open($event)"
|
@contextmenu="contextMenuActions.open($event)"
|
||||||
@click="emit('click')"
|
@click="emit('click')"
|
||||||
>
|
>
|
||||||
@@ -232,7 +232,7 @@ onBeforeMount(contextMenuActions.close);
|
|||||||
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-n-slate-10">
|
<span class="text-xs text-n-slate-10">
|
||||||
{{ lastActivityAt }}
|
{{ lastActivityAt }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,221 +1,236 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { computed, ref, watch, onMounted, nextTick } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAlert, useTrack } from 'dashboard/composables';
|
import { useAlert, useTrack } from 'dashboard/composables';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
import InboxCard from 'dashboard/components-next/Inbox/InboxCard.vue';
|
import InboxCard from 'dashboard/components-next/Inbox/InboxCard.vue';
|
||||||
import InboxListHeader from './components/InboxListHeader.vue';
|
import InboxListHeader from './components/InboxListHeader.vue';
|
||||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
|
||||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
|
|
||||||
export default {
|
const { t } = useI18n();
|
||||||
components: {
|
const route = useRoute();
|
||||||
InboxCard,
|
const router = useRouter();
|
||||||
InboxListHeader,
|
const store = useStore();
|
||||||
IntersectionObserver,
|
const { uiSettings } = useUISettings();
|
||||||
CmdBarConversationSnooze,
|
|
||||||
Spinner,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { uiSettings } = useUISettings();
|
|
||||||
|
|
||||||
return {
|
const notificationList = ref(null);
|
||||||
uiSettings,
|
const page = ref(1);
|
||||||
};
|
const status = ref('');
|
||||||
},
|
const type = ref('');
|
||||||
data() {
|
const sortOrder = ref(wootConstants.INBOX_SORT_BY.NEWEST);
|
||||||
return {
|
const isInboxContextMenuOpen = ref(false);
|
||||||
infiniteLoaderOptions: {
|
|
||||||
root: this.$refs.notificationList,
|
|
||||||
rootMargin: '100px 0px 100px 0px',
|
|
||||||
},
|
|
||||||
page: 1,
|
|
||||||
status: '',
|
|
||||||
type: '',
|
|
||||||
sortOrder: wootConstants.INBOX_SORT_BY.NEWEST,
|
|
||||||
isInboxContextMenuOpen: false,
|
|
||||||
notificationIdToSnooze: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
meta: 'notifications/getMeta',
|
|
||||||
uiFlags: 'notifications/getUIFlags',
|
|
||||||
notification: 'notifications/getFilteredNotifications',
|
|
||||||
notificationV4: 'notifications/getFilteredNotificationsV4',
|
|
||||||
inboxById: 'inboxes/getInboxById',
|
|
||||||
}),
|
|
||||||
currentNotificationId() {
|
|
||||||
return Number(this.$route.params.notification_id);
|
|
||||||
},
|
|
||||||
inboxFilters() {
|
|
||||||
return {
|
|
||||||
page: this.page,
|
|
||||||
status: this.status,
|
|
||||||
type: this.type,
|
|
||||||
sortOrder: this.sortOrder,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
notifications() {
|
|
||||||
return this.notification(this.inboxFilters);
|
|
||||||
},
|
|
||||||
notificationsV4() {
|
|
||||||
return this.notificationV4(this.inboxFilters);
|
|
||||||
},
|
|
||||||
showEndOfList() {
|
|
||||||
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
|
||||||
},
|
|
||||||
showEmptyState() {
|
|
||||||
return !this.uiFlags.isFetching && !this.notifications.length;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
inboxFilters(newVal, oldVal) {
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
this.$store.dispatch('notifications/updateNotificationFilters', newVal);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setSavedFilter();
|
|
||||||
this.fetchNotifications();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
stateInbox(inboxId) {
|
|
||||||
return this.inboxById(inboxId);
|
|
||||||
},
|
|
||||||
fetchNotifications() {
|
|
||||||
this.page = 1;
|
|
||||||
this.$store.dispatch('notifications/clear');
|
|
||||||
const filter = this.inboxFilters;
|
|
||||||
this.$store.dispatch('notifications/index', filter);
|
|
||||||
},
|
|
||||||
redirectToInbox() {
|
|
||||||
if (this.$route.name === 'inbox_view') return;
|
|
||||||
this.$router.replace({ name: 'inbox_view' });
|
|
||||||
},
|
|
||||||
loadMoreNotifications() {
|
|
||||||
if (this.uiFlags.isAllNotificationsLoaded) return;
|
|
||||||
this.$store.dispatch('notifications/index', {
|
|
||||||
page: this.page + 1,
|
|
||||||
status: this.status,
|
|
||||||
type: this.type,
|
|
||||||
sortOrder: this.sortOrder,
|
|
||||||
});
|
|
||||||
this.page += 1;
|
|
||||||
},
|
|
||||||
markNotificationAsRead(notification) {
|
|
||||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
primary_actor_id: primaryActorId,
|
|
||||||
primary_actor_type: primaryActorType,
|
|
||||||
} = notification;
|
|
||||||
this.$store
|
|
||||||
.dispatch('notifications/read', {
|
|
||||||
id,
|
|
||||||
primaryActorId,
|
|
||||||
primaryActorType,
|
|
||||||
unreadCount: this.meta.unreadCount,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
|
|
||||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
|
||||||
});
|
|
||||||
},
|
|
||||||
markNotificationAsUnRead(notification) {
|
|
||||||
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
|
||||||
this.redirectToInbox();
|
|
||||||
const { id } = notification;
|
|
||||||
this.$store
|
|
||||||
.dispatch('notifications/unread', {
|
|
||||||
id,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
|
||||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteNotification(notification) {
|
|
||||||
useTrack(INBOX_EVENTS.DELETE_NOTIFICATION);
|
|
||||||
this.redirectToInbox();
|
|
||||||
this.$store
|
|
||||||
.dispatch('notifications/delete', {
|
|
||||||
notification,
|
|
||||||
unread_count: this.meta.unreadCount,
|
|
||||||
count: this.meta.count,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
useAlert(this.$t('INBOX.ALERTS.DELETE'));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onFilterChange(option) {
|
|
||||||
const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE;
|
|
||||||
if (option.type === STATUS) {
|
|
||||||
this.status = option.selected ? option.key : '';
|
|
||||||
}
|
|
||||||
if (option.type === TYPE) {
|
|
||||||
this.type = option.selected ? option.key : '';
|
|
||||||
}
|
|
||||||
if (option.type === SORT_ORDER) {
|
|
||||||
this.sortOrder = option.key;
|
|
||||||
}
|
|
||||||
this.fetchNotifications();
|
|
||||||
},
|
|
||||||
setSavedFilter() {
|
|
||||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
|
||||||
const { status, type, sort_by: sortBy } = filterBy;
|
|
||||||
this.status = status;
|
|
||||||
this.type = type;
|
|
||||||
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
|
||||||
this.$store.dispatch(
|
|
||||||
'notifications/setNotificationFilters',
|
|
||||||
this.inboxFilters
|
|
||||||
);
|
|
||||||
},
|
|
||||||
openConversation(notification) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
primaryActorId,
|
|
||||||
primaryActorType,
|
|
||||||
primaryActor: { inboxId },
|
|
||||||
notificationType,
|
|
||||||
} = notification;
|
|
||||||
|
|
||||||
if (this.$route.params.notification_id !== id) {
|
const infiniteLoaderOptions = computed(() => ({
|
||||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
root: notificationList.value,
|
||||||
notificationType,
|
rootMargin: '100px 0px 100px 0px',
|
||||||
});
|
}));
|
||||||
|
|
||||||
this.$store
|
const meta = useMapGetter('notifications/getMeta');
|
||||||
.dispatch('notifications/read', {
|
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||||
id,
|
const records = useMapGetter('notifications/getFilteredNotificationsV4');
|
||||||
primaryActorId,
|
const inboxById = useMapGetter('inboxes/getInboxById');
|
||||||
primaryActorType,
|
|
||||||
unreadCount: this.meta.unreadCount,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$router.push({
|
const currentConversationId = computed(() => Number(route.params.id));
|
||||||
name: 'inbox_view_conversation',
|
|
||||||
params: { inboxId, notification_id: id },
|
const inboxFilters = computed(() => ({
|
||||||
});
|
page: page.value,
|
||||||
}
|
status: status.value,
|
||||||
},
|
type: type.value,
|
||||||
},
|
sortOrder: sortOrder.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const notifications = computed(() => {
|
||||||
|
return records.value(inboxFilters.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const showEndOfList = computed(() => {
|
||||||
|
return uiFlags.value.isAllNotificationsLoaded && !uiFlags.value.isFetching;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showEmptyState = computed(() => {
|
||||||
|
return !uiFlags.value.isFetching && !notifications.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateInbox = inboxId => {
|
||||||
|
return inboxById.value(inboxId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchNotifications = () => {
|
||||||
|
page.value = 1;
|
||||||
|
store.dispatch('notifications/clear');
|
||||||
|
const filter = inboxFilters.value;
|
||||||
|
store.dispatch('notifications/index', filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollActiveIntoView = () => {
|
||||||
|
const activeEl = notificationList.value?.querySelector('.inbox-card.active');
|
||||||
|
activeEl?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToInbox = () => {
|
||||||
|
if (route.name === 'inbox_view') return;
|
||||||
|
router.replace({ name: 'inbox_view' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreNotifications = () => {
|
||||||
|
if (uiFlags.value.isAllNotificationsLoaded) return;
|
||||||
|
|
||||||
|
page.value += 1;
|
||||||
|
store.dispatch('notifications/index', {
|
||||||
|
page: page.value,
|
||||||
|
status: status.value,
|
||||||
|
type: type.value,
|
||||||
|
sortOrder: sortOrder.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const markNotificationAsRead = async notificationItem => {
|
||||||
|
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
} = notificationItem;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('notifications/read', {
|
||||||
|
id,
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount: meta.value.unreadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAlert(t('INBOX.ALERTS.MARK_AS_READ'));
|
||||||
|
store.dispatch('notifications/unReadCount');
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markNotificationAsUnRead = async notificationItem => {
|
||||||
|
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
||||||
|
redirectToInbox();
|
||||||
|
|
||||||
|
const { id } = notificationItem;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('notifications/unread', { id });
|
||||||
|
useAlert(t('INBOX.ALERTS.MARK_AS_UNREAD'));
|
||||||
|
store.dispatch('notifications/unReadCount');
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNotification = async notificationItem => {
|
||||||
|
useTrack(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||||
|
redirectToInbox();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('notifications/delete', {
|
||||||
|
notification: notificationItem,
|
||||||
|
unread_count: meta.value.unreadCount,
|
||||||
|
count: meta.value.count,
|
||||||
|
});
|
||||||
|
|
||||||
|
useAlert(t('INBOX.ALERTS.DELETE'));
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterChange = option => {
|
||||||
|
const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE;
|
||||||
|
if (option.type === STATUS) {
|
||||||
|
status.value = option.selected ? option.key : '';
|
||||||
|
}
|
||||||
|
if (option.type === TYPE) {
|
||||||
|
type.value = option.selected ? option.key : '';
|
||||||
|
}
|
||||||
|
if (option.type === SORT_ORDER) {
|
||||||
|
sortOrder.value = option.key;
|
||||||
|
}
|
||||||
|
fetchNotifications();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSavedFilter = () => {
|
||||||
|
const { inbox_filter_by: filterBy = {} } = uiSettings.value;
|
||||||
|
const { status: savedStatus, type: savedType, sort_by: sortBy } = filterBy;
|
||||||
|
status.value = savedStatus;
|
||||||
|
type.value = savedType;
|
||||||
|
sortOrder.value = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
|
||||||
|
store.dispatch('notifications/setNotificationFilters', inboxFilters.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openConversation = async notificationItem => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
primaryActor: { inboxId, id: conversationId },
|
||||||
|
notificationType,
|
||||||
|
} = notificationItem;
|
||||||
|
|
||||||
|
if (route.params.id === String(conversationId)) return;
|
||||||
|
|
||||||
|
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||||
|
notificationType,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('notifications/read', {
|
||||||
|
id,
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount: meta.value.unreadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
// to update the unread count in the store realtime
|
||||||
|
store.dispatch('notifications/unReadCount');
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'inbox_view_conversation',
|
||||||
|
params: { inboxId, type: 'conversation', id: conversationId },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
inboxFilters,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
store.dispatch('notifications/updateNotificationFilters', newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(currentConversationId, () => {
|
||||||
|
nextTick(scrollActiveIntoView);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollActiveIntoView();
|
||||||
|
setSavedFilter();
|
||||||
|
fetchNotifications();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="flex w-full h-full bg-n-solid-1">
|
<section class="flex w-full h-full bg-n-solid-1">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col h-full w-full lg:min-w-[400px] lg:max-w-[400px] ltr:border-r rtl:border-l border-n-weak"
|
class="flex flex-col h-full w-full lg:min-w-[340px] lg:max-w-[340px] ltr:border-r rtl:border-l border-n-weak"
|
||||||
:class="!currentNotificationId ? 'flex' : 'hidden xl:flex'"
|
:class="!currentConversationId ? 'flex' : 'hidden xl:flex'"
|
||||||
>
|
>
|
||||||
<InboxListHeader
|
<InboxListHeader
|
||||||
:is-context-menu-open="isInboxContextMenuOpen"
|
:is-context-menu-open="isInboxContextMenuOpen"
|
||||||
@@ -224,17 +239,17 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="notificationList"
|
ref="notificationList"
|
||||||
class="flex flex-col gap-px w-full h-[calc(100%-56px)] pb-3 overflow-x-hidden px-3 overflow-y-auto divide-y divide-n-weak [&>*:hover]:!border-y-transparent [&>*.active]:!border-y-transparent [&>*:hover+*]:!border-t-transparent [&>*.active+*]:!border-t-transparent"
|
class="flex flex-col gap-0.5 w-full h-[calc(100%-56px)] pb-4 overflow-x-hidden px-2 overflow-y-auto divide-y divide-n-weak [&>*:hover]:!border-y-transparent [&>*.active]:!border-y-transparent [&>*:hover+*]:!border-t-transparent [&>*.active+*]:!border-t-transparent"
|
||||||
>
|
>
|
||||||
<InboxCard
|
<InboxCard
|
||||||
v-for="notificationItem in notificationsV4"
|
v-for="notificationItem in notifications"
|
||||||
:key="notificationItem.id"
|
:key="notificationItem.id"
|
||||||
:inbox-item="notificationItem"
|
:inbox-item="notificationItem"
|
||||||
:state-inbox="stateInbox(notificationItem.primaryActor?.inboxId)"
|
:state-inbox="stateInbox(notificationItem.primaryActor?.inboxId)"
|
||||||
class="rounded-none hover:rounded-xl hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
|
class="inbox-card rounded-lg hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3"
|
||||||
:class="
|
:class="
|
||||||
currentNotificationId === notificationItem.id
|
currentConversationId === notificationItem.primaryActor?.id
|
||||||
? 'bg-n-alpha-1 dark:bg-n-alpha-3 click-animation rounded-xl active'
|
? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
@mark-notification-as-read="markNotificationAsRead"
|
@mark-notification-as-read="markNotificationAsRead"
|
||||||
@@ -264,23 +279,3 @@ export default {
|
|||||||
<CmdBarConversationSnooze />
|
<CmdBarConversationSnooze />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.click-animation {
|
|
||||||
animation: click-animation 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes click-animation {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
transform: scale(0.99);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,183 +1,191 @@
|
|||||||
<script>
|
<script setup>
|
||||||
import { mapGetters } from 'vuex';
|
import { computed, ref, watch, onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useTrack } from 'dashboard/composables';
|
import { useTrack } from 'dashboard/composables';
|
||||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
|
||||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
|
||||||
import InboxEmptyState from './InboxEmptyState.vue';
|
|
||||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
import SidepanelSwitch from 'dashboard/components-next/Conversation/SidepanelSwitch.vue';
|
||||||
|
|
||||||
export default {
|
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||||
components: {
|
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
|
||||||
InboxItemHeader,
|
import InboxEmptyState from './InboxEmptyState.vue';
|
||||||
InboxEmptyState,
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
ConversationBox,
|
import ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
|
||||||
Spinner,
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { uiSettings, updateUISettings } = useUISettings();
|
|
||||||
|
|
||||||
return {
|
const route = useRoute();
|
||||||
uiSettings,
|
const router = useRouter();
|
||||||
updateUISettings,
|
const store = useStore();
|
||||||
};
|
const { uiSettings } = useUISettings();
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isConversationLoading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
notification: 'notifications/getFilteredNotifications',
|
|
||||||
currentChat: 'getSelectedChat',
|
|
||||||
activeNotificationById: 'notifications/getNotificationById',
|
|
||||||
conversationById: 'getConversationById',
|
|
||||||
uiFlags: 'notifications/getUIFlags',
|
|
||||||
meta: 'notifications/getMeta',
|
|
||||||
}),
|
|
||||||
notifications() {
|
|
||||||
return this.notification({
|
|
||||||
sortOrder: this.activeSortOrder,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
inboxId() {
|
|
||||||
return Number(this.$route.params.inboxId);
|
|
||||||
},
|
|
||||||
notificationId() {
|
|
||||||
return Number(this.$route.params.notification_id);
|
|
||||||
},
|
|
||||||
activeNotification() {
|
|
||||||
return this.activeNotificationById(this.notificationId);
|
|
||||||
},
|
|
||||||
conversationId() {
|
|
||||||
return this.activeNotification?.primary_actor?.id;
|
|
||||||
},
|
|
||||||
totalNotificationCount() {
|
|
||||||
return this.meta.count;
|
|
||||||
},
|
|
||||||
showEmptyState() {
|
|
||||||
return (
|
|
||||||
!this.conversationId ||
|
|
||||||
(!this.notifications?.length && this.uiFlags.isFetching)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
activeNotificationIndex() {
|
|
||||||
return this.notifications?.findIndex(n => n.id === this.notificationId);
|
|
||||||
},
|
|
||||||
activeSortOrder() {
|
|
||||||
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
|
|
||||||
const { sort_by: sortBy } = filterBy;
|
|
||||||
return sortBy || 'desc';
|
|
||||||
},
|
|
||||||
isContactPanelOpen() {
|
|
||||||
if (this.currentChat.id) {
|
|
||||||
const { is_contact_sidebar_open: isContactSidebarOpen } =
|
|
||||||
this.uiSettings;
|
|
||||||
return isContactSidebarOpen;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
conversationId: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal, oldVal) {
|
|
||||||
if (newVal !== oldVal) {
|
|
||||||
this.fetchConversationById();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('agents/get');
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchConversationById() {
|
|
||||||
if (!this.notificationId || !this.conversationId) return;
|
|
||||||
this.$store.dispatch('clearSelectedState');
|
|
||||||
const existingChat = this.findConversation();
|
|
||||||
if (existingChat) {
|
|
||||||
this.setActiveChat(existingChat);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isConversationLoading = true;
|
|
||||||
await this.$store.dispatch('getConversation', this.conversationId);
|
|
||||||
this.setActiveChat();
|
|
||||||
this.isConversationLoading = false;
|
|
||||||
},
|
|
||||||
setActiveChat() {
|
|
||||||
const selectedConversation = this.findConversation();
|
|
||||||
if (!selectedConversation) return;
|
|
||||||
this.$store
|
|
||||||
.dispatch('setActiveChat', { data: selectedConversation })
|
|
||||||
.then(() => {
|
|
||||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
findConversation() {
|
|
||||||
return this.conversationById(this.conversationId);
|
|
||||||
},
|
|
||||||
navigateToConversation(activeIndex, direction) {
|
|
||||||
let updatedIndex;
|
|
||||||
if (direction === 'prev' && activeIndex) {
|
|
||||||
updatedIndex = activeIndex - 1;
|
|
||||||
} else if (
|
|
||||||
direction === 'next' &&
|
|
||||||
activeIndex < this.totalNotificationCount
|
|
||||||
) {
|
|
||||||
updatedIndex = activeIndex + 1;
|
|
||||||
}
|
|
||||||
const targetNotification = this.notifications[updatedIndex];
|
|
||||||
if (targetNotification) {
|
|
||||||
this.openNotification(targetNotification);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openNotification(notification) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
primary_actor_id: primaryActorId,
|
|
||||||
primary_actor_type: primaryActorType,
|
|
||||||
primary_actor: { meta: { unreadCount } = {} },
|
|
||||||
notification_type: notificationType,
|
|
||||||
} = notification;
|
|
||||||
|
|
||||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
const isConversationLoading = ref(false);
|
||||||
notificationType,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$store.dispatch('notifications/read', {
|
const notification = useMapGetter('notifications/getFilteredNotifications');
|
||||||
id,
|
const currentChat = useMapGetter('getSelectedChat');
|
||||||
primaryActorId,
|
const conversationById = useMapGetter('getConversationById');
|
||||||
primaryActorType,
|
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||||
unreadCount,
|
const meta = useMapGetter('notifications/getMeta');
|
||||||
});
|
|
||||||
|
|
||||||
this.$router.push({
|
const inboxId = computed(() => Number(route.params.inboxId));
|
||||||
name: 'inbox_view_conversation',
|
const conversationId = computed(() => Number(route.params.id));
|
||||||
params: { notification_id: id },
|
|
||||||
});
|
const activeSortOrder = computed(() => {
|
||||||
},
|
const { inbox_filter_by: filterBy = {} } = uiSettings.value;
|
||||||
onClickNext() {
|
const { sort_by: sortBy } = filterBy;
|
||||||
this.navigateToConversation(this.activeNotificationIndex, 'next');
|
return sortBy || 'desc';
|
||||||
},
|
});
|
||||||
onClickPrev() {
|
|
||||||
this.navigateToConversation(this.activeNotificationIndex, 'prev');
|
const notifications = computed(() => {
|
||||||
},
|
return notification.value({
|
||||||
onToggleContactPanel() {
|
sortOrder: activeSortOrder.value,
|
||||||
this.updateUISettings({
|
});
|
||||||
is_contact_sidebar_open: !this.isContactPanelOpen,
|
});
|
||||||
});
|
|
||||||
},
|
const activeNotification = computed(() => {
|
||||||
},
|
return notifications.value?.find(
|
||||||
|
n => n.primary_actor?.id === conversationId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalNotificationCount = computed(() => {
|
||||||
|
return meta.value.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showEmptyState = computed(() => {
|
||||||
|
return (
|
||||||
|
!conversationId.value ||
|
||||||
|
(!notifications.value?.length && uiFlags.value.isFetching)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeNotificationIndex = computed(() => {
|
||||||
|
return notifications.value?.findIndex(
|
||||||
|
n => n.primary_actor?.id === conversationId.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isContactPanelOpen = computed(() => {
|
||||||
|
if (currentChat.value.id) {
|
||||||
|
const { is_contact_sidebar_open: isContactSidebarOpen } = uiSettings.value;
|
||||||
|
return isContactSidebarOpen;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const findConversation = () => {
|
||||||
|
return conversationById.value(conversationId.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openNotification = async notificationItem => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
primary_actor: {
|
||||||
|
meta: { unreadCount } = {},
|
||||||
|
id: conversationIdFromNotification,
|
||||||
|
},
|
||||||
|
notification_type: notificationType,
|
||||||
|
} = notificationItem;
|
||||||
|
|
||||||
|
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||||
|
notificationType,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('notifications/read', {
|
||||||
|
id,
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
name: 'inbox_view_conversation',
|
||||||
|
params: { type: 'conversation', id: conversationIdFromNotification },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveChat = async () => {
|
||||||
|
const selectedConversation = findConversation();
|
||||||
|
if (!selectedConversation) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('setActiveChat', { data: selectedConversation });
|
||||||
|
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchConversationById = async () => {
|
||||||
|
if (!conversationId.value) return;
|
||||||
|
|
||||||
|
store.dispatch('clearSelectedState');
|
||||||
|
const existingChat = findConversation();
|
||||||
|
|
||||||
|
if (existingChat) {
|
||||||
|
await setActiveChat();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConversationLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.dispatch('getConversation', conversationId.value);
|
||||||
|
await setActiveChat();
|
||||||
|
} catch {
|
||||||
|
// error
|
||||||
|
} finally {
|
||||||
|
isConversationLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToConversation = (activeIndex, direction) => {
|
||||||
|
const isValidPrev = direction === 'prev' && activeIndex > 0;
|
||||||
|
const isValidNext =
|
||||||
|
direction === 'next' && activeIndex < totalNotificationCount.value - 1;
|
||||||
|
|
||||||
|
if (!isValidPrev && !isValidNext) return;
|
||||||
|
|
||||||
|
const updatedIndex = direction === 'prev' ? activeIndex - 1 : activeIndex + 1;
|
||||||
|
const targetNotification = notifications.value[updatedIndex];
|
||||||
|
|
||||||
|
if (targetNotification) {
|
||||||
|
openNotification(targetNotification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickNext = () => {
|
||||||
|
navigateToConversation(activeNotificationIndex.value, 'next');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickPrev = () => {
|
||||||
|
navigateToConversation(activeNotificationIndex.value, 'prev');
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
conversationId,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
fetchConversationById();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await store.dispatch('agents/get');
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full xl:w-[calc(100%-400px)]">
|
<div class="h-full w-full flex-1">
|
||||||
<div v-if="showEmptyState" class="flex w-full h-full">
|
<div v-if="showEmptyState" class="flex w-full h-full">
|
||||||
<InboxEmptyState
|
<InboxEmptyState
|
||||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||||
@@ -193,19 +201,24 @@ export default {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="isConversationLoading"
|
v-if="isConversationLoading"
|
||||||
class="flex items-center h-[calc(100%-56px)] my-4 justify-center bg-n-solid-1"
|
class="flex items-center flex-1 my-4 justify-center bg-n-solid-1"
|
||||||
>
|
>
|
||||||
<Spinner class="text-n-brand" />
|
<Spinner class="text-n-brand" />
|
||||||
</div>
|
</div>
|
||||||
<ConversationBox
|
<div v-else class="flex h-[calc(100%-48px)] min-w-0">
|
||||||
v-else
|
<ConversationBox
|
||||||
class="h-[calc(100%-56px)] [&.conversation-details-wrap]:!border-0"
|
class="flex-1 [&.conversation-details-wrap]:!border-0"
|
||||||
is-inbox-view
|
is-inbox-view
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-on-expanded-layout="false"
|
||||||
:is-on-expanded-layout="false"
|
>
|
||||||
@contact-panel-toggle="onToggleContactPanel"
|
<SidepanelSwitch v-if="currentChat.id" />
|
||||||
/>
|
</ConversationBox>
|
||||||
|
<ConversationSidebar
|
||||||
|
v-if="isContactPanelOpen"
|
||||||
|
:current-chat="currentChat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between w-full gap-2 border-b ltr:pl-4 rtl:pl-2 h-12 ltr:pr-2 rtl:pr-4 rtl:border-r border-n-weak"
|
class="flex items-center justify-between w-full gap-2 border-b px-3 h-12 rtl:border-r border-n-weak flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<BackButton
|
<BackButton
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center justify-between w-full gap-1 h-14 px-4 mb-2">
|
<div class="flex items-center justify-between w-full gap-1 h-12 px-3">
|
||||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<h1 class="min-w-0 text-lg font-medium truncate text-n-slate-12">
|
<h1 class="min-w-0 text-base font-medium truncate text-n-slate-12">
|
||||||
{{ $t('INBOX.LIST.TITLE') }}
|
{{ $t('INBOX.LIST.TITLE') }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const routes = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':notification_id',
|
path: ':type/:id',
|
||||||
name: 'inbox_view_conversation',
|
name: 'inbox_view_conversation',
|
||||||
component: InboxDetailView,
|
component: InboxDetailView,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
Reference in New Issue
Block a user