mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48: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>
|
||||
<div
|
||||
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)"
|
||||
@click="emit('click')"
|
||||
>
|
||||
@@ -232,7 +232,7 @@ onBeforeMount(contextMenuActions.close);
|
||||
class="flex-shrink-0 text-n-slate-11 size-2.5"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-10">
|
||||
<span class="text-xs text-n-slate-10">
|
||||
{{ lastActivityAt }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,221 +1,236 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
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 { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import InboxCard from 'dashboard/components-next/Inbox/InboxCard.vue';
|
||||
import InboxListHeader from './components/InboxListHeader.vue';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||
import CmdBarConversationSnooze from 'dashboard/routes/dashboard/commands/CmdBarConversationSnooze.vue';
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxCard,
|
||||
InboxListHeader,
|
||||
IntersectionObserver,
|
||||
CmdBarConversationSnooze,
|
||||
Spinner,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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;
|
||||
const notificationList = ref(null);
|
||||
const page = ref(1);
|
||||
const status = ref('');
|
||||
const type = ref('');
|
||||
const sortOrder = ref(wootConstants.INBOX_SORT_BY.NEWEST);
|
||||
const isInboxContextMenuOpen = ref(false);
|
||||
|
||||
if (this.$route.params.notification_id !== id) {
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
const infiniteLoaderOptions = computed(() => ({
|
||||
root: notificationList.value,
|
||||
rootMargin: '100px 0px 100px 0px',
|
||||
}));
|
||||
|
||||
this.$store
|
||||
.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
})
|
||||
.then(() => {
|
||||
this.$store.dispatch('notifications/unReadCount'); // to update the unread count in the store real time
|
||||
});
|
||||
const meta = useMapGetter('notifications/getMeta');
|
||||
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||
const records = useMapGetter('notifications/getFilteredNotificationsV4');
|
||||
const inboxById = useMapGetter('inboxes/getInboxById');
|
||||
|
||||
this.$router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { inboxId, notification_id: id },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
const currentConversationId = computed(() => Number(route.params.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>
|
||||
|
||||
<template>
|
||||
<section class="flex w-full h-full bg-n-solid-1">
|
||||
<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="!currentNotificationId ? 'flex' : 'hidden xl:flex'"
|
||||
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="!currentConversationId ? 'flex' : 'hidden xl:flex'"
|
||||
>
|
||||
<InboxListHeader
|
||||
:is-context-menu-open="isInboxContextMenuOpen"
|
||||
@@ -224,17 +239,17 @@ export default {
|
||||
/>
|
||||
<div
|
||||
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
|
||||
v-for="notificationItem in notificationsV4"
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
:inbox-item="notificationItem"
|
||||
: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="
|
||||
currentNotificationId === notificationItem.id
|
||||
? 'bg-n-alpha-1 dark:bg-n-alpha-3 click-animation rounded-xl active'
|
||||
currentConversationId === notificationItem.primaryActor?.id
|
||||
? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active'
|
||||
: ''
|
||||
"
|
||||
@mark-notification-as-read="markNotificationAsRead"
|
||||
@@ -264,23 +279,3 @@ export default {
|
||||
<CmdBarConversationSnooze />
|
||||
</section>
|
||||
</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>
|
||||
import { mapGetters } from 'vuex';
|
||||
<script setup>
|
||||
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 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 { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import SidepanelSwitch from 'dashboard/components-next/Conversation/SidepanelSwitch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxItemHeader,
|
||||
InboxEmptyState,
|
||||
ConversationBox,
|
||||
Spinner,
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
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 ConversationSidebar from 'dashboard/components/widgets/conversation/ConversationSidebar.vue';
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
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;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
useTrack(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
|
||||
notificationType,
|
||||
});
|
||||
const isConversationLoading = ref(false);
|
||||
|
||||
this.$store.dispatch('notifications/read', {
|
||||
id,
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount,
|
||||
});
|
||||
const notification = useMapGetter('notifications/getFilteredNotifications');
|
||||
const currentChat = useMapGetter('getSelectedChat');
|
||||
const conversationById = useMapGetter('getConversationById');
|
||||
const uiFlags = useMapGetter('notifications/getUIFlags');
|
||||
const meta = useMapGetter('notifications/getMeta');
|
||||
|
||||
this.$router.push({
|
||||
name: 'inbox_view_conversation',
|
||||
params: { notification_id: id },
|
||||
});
|
||||
},
|
||||
onClickNext() {
|
||||
this.navigateToConversation(this.activeNotificationIndex, 'next');
|
||||
},
|
||||
onClickPrev() {
|
||||
this.navigateToConversation(this.activeNotificationIndex, 'prev');
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.updateUISettings({
|
||||
is_contact_sidebar_open: !this.isContactPanelOpen,
|
||||
});
|
||||
},
|
||||
},
|
||||
const inboxId = computed(() => Number(route.params.inboxId));
|
||||
const conversationId = computed(() => Number(route.params.id));
|
||||
|
||||
const activeSortOrder = computed(() => {
|
||||
const { inbox_filter_by: filterBy = {} } = uiSettings.value;
|
||||
const { sort_by: sortBy } = filterBy;
|
||||
return sortBy || 'desc';
|
||||
});
|
||||
|
||||
const notifications = computed(() => {
|
||||
return notification.value({
|
||||
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 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>
|
||||
|
||||
<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">
|
||||
<InboxEmptyState
|
||||
:empty-state-message="$t('INBOX.LIST.NO_MESSAGES_AVAILABLE')"
|
||||
@@ -193,19 +201,24 @@ export default {
|
||||
/>
|
||||
<div
|
||||
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" />
|
||||
</div>
|
||||
<ConversationBox
|
||||
v-else
|
||||
class="h-[calc(100%-56px)] [&.conversation-details-wrap]:!border-0"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="false"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<div v-else class="flex h-[calc(100%-48px)] min-w-0">
|
||||
<ConversationBox
|
||||
class="flex-1 [&.conversation-details-wrap]:!border-0"
|
||||
is-inbox-view
|
||||
:inbox-id="inboxId"
|
||||
:is-on-expanded-layout="false"
|
||||
>
|
||||
<SidepanelSwitch v-if="currentChat.id" />
|
||||
</ConversationBox>
|
||||
<ConversationSidebar
|
||||
v-if="isContactPanelOpen"
|
||||
:current-chat="currentChat"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,7 +109,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<BackButton
|
||||
|
||||
@@ -79,9 +79,9 @@ export default {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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') }}
|
||||
</h1>
|
||||
<div class="relative">
|
||||
|
||||
@@ -21,7 +21,7 @@ export const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':notification_id',
|
||||
path: ':type/:id',
|
||||
name: 'inbox_view_conversation',
|
||||
component: InboxDetailView,
|
||||
meta: {
|
||||
|
||||
Reference in New Issue
Block a user