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:
Sivin Varghese
2025-08-01 04:44:04 +05:30
committed by GitHub
parent 446a219cd1
commit c98c255ed0
6 changed files with 413 additions and 405 deletions

View File

@@ -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>

View File

@@ -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,
CmdBarConversationSnooze,
Spinner,
},
setup() {
const { uiSettings } = useUISettings(); 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, const infiniteLoaderOptions = computed(() => ({
root: notificationList.value,
rootMargin: '100px 0px 100px 0px', rootMargin: '100px 0px 100px 0px',
}, }));
page: 1,
status: '', const meta = useMapGetter('notifications/getMeta');
type: '', const uiFlags = useMapGetter('notifications/getUIFlags');
sortOrder: wootConstants.INBOX_SORT_BY.NEWEST, const records = useMapGetter('notifications/getFilteredNotificationsV4');
isInboxContextMenuOpen: false, const inboxById = useMapGetter('inboxes/getInboxById');
notificationIdToSnooze: null,
}; const currentConversationId = computed(() => Number(route.params.id));
},
computed: { const inboxFilters = computed(() => ({
...mapGetters({ page: page.value,
meta: 'notifications/getMeta', status: status.value,
uiFlags: 'notifications/getUIFlags', type: type.value,
notification: 'notifications/getFilteredNotifications', sortOrder: sortOrder.value,
notificationV4: 'notifications/getFilteredNotificationsV4', }));
inboxById: 'inboxes/getInboxById',
}), const notifications = computed(() => {
currentNotificationId() { return records.value(inboxFilters.value);
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;
}, const showEndOfList = computed(() => {
markNotificationAsRead(notification) { 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); useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
const { const {
id, id,
primary_actor_id: primaryActorId, primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType, primary_actor_type: primaryActorType,
} = notification; } = notificationItem;
this.$store
.dispatch('notifications/read', { try {
await store.dispatch('notifications/read', {
id, id,
primaryActorId, primaryActorId,
primaryActorType, primaryActorType,
unreadCount: this.meta.unreadCount, unreadCount: meta.value.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) { useAlert(t('INBOX.ALERTS.MARK_AS_READ'));
store.dispatch('notifications/unReadCount');
} catch {
// error
}
};
const markNotificationAsUnRead = async notificationItem => {
useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD); useTrack(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
this.redirectToInbox(); redirectToInbox();
const { id } = notification;
this.$store const { id } = notificationItem;
.dispatch('notifications/unread', {
id, try {
}) await store.dispatch('notifications/unread', { id });
.then(() => { useAlert(t('INBOX.ALERTS.MARK_AS_UNREAD'));
useAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD')); store.dispatch('notifications/unReadCount');
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time } catch {
}); // error
}, }
deleteNotification(notification) { };
const deleteNotification = async notificationItem => {
useTrack(INBOX_EVENTS.DELETE_NOTIFICATION); useTrack(INBOX_EVENTS.DELETE_NOTIFICATION);
this.redirectToInbox(); redirectToInbox();
this.$store
.dispatch('notifications/delete', { try {
notification, await store.dispatch('notifications/delete', {
unread_count: this.meta.unreadCount, notification: notificationItem,
count: this.meta.count, unread_count: meta.value.unreadCount,
}) count: meta.value.count,
.then(() => {
useAlert(this.$t('INBOX.ALERTS.DELETE'));
}); });
},
onFilterChange(option) { useAlert(t('INBOX.ALERTS.DELETE'));
} catch {
// error
}
};
const onFilterChange = option => {
const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE; const { STATUS, TYPE, SORT_ORDER } = wootConstants.INBOX_FILTER_TYPE;
if (option.type === STATUS) { if (option.type === STATUS) {
this.status = option.selected ? option.key : ''; status.value = option.selected ? option.key : '';
} }
if (option.type === TYPE) { if (option.type === TYPE) {
this.type = option.selected ? option.key : ''; type.value = option.selected ? option.key : '';
} }
if (option.type === SORT_ORDER) { if (option.type === SORT_ORDER) {
this.sortOrder = option.key; sortOrder.value = option.key;
} }
this.fetchNotifications(); fetchNotifications();
}, };
setSavedFilter() {
const { inbox_filter_by: filterBy = {} } = this.uiSettings; const setSavedFilter = () => {
const { status, type, sort_by: sortBy } = filterBy; const { inbox_filter_by: filterBy = {} } = uiSettings.value;
this.status = status; const { status: savedStatus, type: savedType, sort_by: sortBy } = filterBy;
this.type = type; status.value = savedStatus;
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST; type.value = savedType;
this.$store.dispatch( sortOrder.value = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
'notifications/setNotificationFilters', store.dispatch('notifications/setNotificationFilters', inboxFilters.value);
this.inboxFilters };
);
}, const openConversation = async notificationItem => {
openConversation(notification) {
const { const {
id, id,
primaryActorId, primaryActorId,
primaryActorType, primaryActorType,
primaryActor: { inboxId }, primaryActor: { inboxId, id: conversationId },
notificationType, notificationType,
} = notification; } = notificationItem;
if (route.params.id === String(conversationId)) return;
if (this.$route.params.notification_id !== id) {
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, { useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
notificationType, notificationType,
}); });
this.$store try {
.dispatch('notifications/read', { await store.dispatch('notifications/read', {
id, id,
primaryActorId, primaryActorId,
primaryActorType, primaryActorType,
unreadCount: this.meta.unreadCount, unreadCount: meta.value.unreadCount,
})
.then(() => {
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
}); });
this.$router.push({ // to update the unread count in the store realtime
store.dispatch('notifications/unReadCount');
router.push({
name: 'inbox_view_conversation', name: 'inbox_view_conversation',
params: { inboxId, notification_id: id }, 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>

View File

@@ -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() { const isConversationLoading = ref(false);
return {
isConversationLoading: false, const notification = useMapGetter('notifications/getFilteredNotifications');
}; const currentChat = useMapGetter('getSelectedChat');
}, const conversationById = useMapGetter('getConversationById');
computed: { const uiFlags = useMapGetter('notifications/getUIFlags');
...mapGetters({ const meta = useMapGetter('notifications/getMeta');
notification: 'notifications/getFilteredNotifications',
currentChat: 'getSelectedChat', const inboxId = computed(() => Number(route.params.inboxId));
activeNotificationById: 'notifications/getNotificationById', const conversationId = computed(() => Number(route.params.id));
conversationById: 'getConversationById',
uiFlags: 'notifications/getUIFlags', const activeSortOrder = computed(() => {
meta: 'notifications/getMeta', const { inbox_filter_by: filterBy = {} } = uiSettings.value;
}),
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; const { sort_by: sortBy } = filterBy;
return sortBy || 'desc'; return sortBy || 'desc';
}, });
isContactPanelOpen() {
if (this.currentChat.id) { const notifications = computed(() => {
const { is_contact_sidebar_open: isContactSidebarOpen } = return notification.value({
this.uiSettings; sortOrder: activeSortOrder.value,
});
});
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 isContactSidebarOpen;
} }
return false; 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() { const findConversation = () => {
return this.conversationById(this.conversationId); return conversationById.value(conversationId.value);
}, };
navigateToConversation(activeIndex, direction) {
let updatedIndex; const openNotification = async notificationItem => {
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 { const {
id, id,
primary_actor_id: primaryActorId, primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType, primary_actor_type: primaryActorType,
primary_actor: { meta: { unreadCount } = {} }, primary_actor: {
meta: { unreadCount } = {},
id: conversationIdFromNotification,
},
notification_type: notificationType, notification_type: notificationType,
} = notification; } = notificationItem;
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, { useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
notificationType, notificationType,
}); });
this.$store.dispatch('notifications/read', { try {
await store.dispatch('notifications/read', {
id, id,
primaryActorId, primaryActorId,
primaryActorType, primaryActorType,
unreadCount, unreadCount,
}); });
this.$router.push({ router.push({
name: 'inbox_view_conversation', name: 'inbox_view_conversation',
params: { notification_id: id }, params: { type: 'conversation', id: conversationIdFromNotification },
}); });
}, } catch {
onClickNext() { // error
this.navigateToConversation(this.activeNotificationIndex, 'next'); }
},
onClickPrev() {
this.navigateToConversation(this.activeNotificationIndex, 'prev');
},
onToggleContactPanel() {
this.updateUISettings({
is_contact_sidebar_open: !this.isContactPanelOpen,
});
},
},
}; };
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>
<div v-else class="flex h-[calc(100%-48px)] min-w-0">
<ConversationBox <ConversationBox
v-else class="flex-1 [&.conversation-details-wrap]:!border-0"
class="h-[calc(100%-56px)] [&.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>

View File

@@ -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

View File

@@ -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">

View File

@@ -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: {