Files
core/view/tpl/notifications_widget.tpl

788 lines
27 KiB
Smarty

<script>
var sse_bs_active = false;
var sse_offset = 0;
var sse_type;
var sse_partial_result = false;
var sse_rmids = [];
var sse_fallback_interval;
var sse_sys_only = {{$sys_only}};
document.addEventListener("DOMContentLoaded", function() {
let notificationsWrapper = document.getElementById('notifications_wrapper');
let notificationsParent = notificationsWrapper ? notificationsWrapper.parentElement.id : null;
let notificationsBtn = document.querySelectorAll('.notifications-btn');
// Event listener for notifications button
if (notificationsBtn) {
notificationsBtn.forEach(function (element) {
element.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Remove the 'd-none' class to show the notifications wrapper
notificationsWrapper.classList.remove('d-none');
// Check if the notifications wrapper has the 'fs' class
if (notificationsWrapper.classList.contains('fs')) {
// Prepend the notifications wrapper back to its original parent and hide it
document.getElementById(notificationsParent).appendChild(notificationsWrapper);
notificationsWrapper.classList.add('d-none');
} else {
// Otherwise, prepend the notifications wrapper to 'main'
document.querySelector('main').prepend(notificationsWrapper);
}
// Toggle the 'fs' class
notificationsWrapper.classList.toggle('fs');
});
});
}
// Event listener for clicking a notification
document.addEventListener('click', function(event) {
if (event.target.closest('a') && event.target.closest('a').classList.contains('notification')) {
console.log(1)
if (notificationsWrapper.classList.contains('fs')) {
// Move notifications wrapper back to its original parent and hide it
notificationsWrapper.classList.remove('fs');
notificationsWrapper.classList.add('d-none');
document.getElementById(notificationsParent).appendChild(notificationsWrapper);
}
}
});
if(sse_enabled) {
if(typeof(window.SharedWorker) === 'undefined') {
// notifications with multiple tabs open will not work very well in this scenario
let evtSource = new EventSource('/sse');
evtSource.addEventListener('notifications', function(e) {
let obj = JSON.parse(e.data);
sse_handleNotifications(obj, false, false);
}, false);
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
sse_offset = 0;
sse_bs_init();
}
}, false);
}
else {
let myWorker = new SharedWorker('/view/js/sse_worker.js', localUser);
myWorker.port.onmessage = function(e) {
obj = e.data;
console.log(obj);
sse_handleNotifications(obj, false, false);
}
myWorker.onerror = function(e) {
myWorker.port.close();
}
myWorker.port.start();
}
}
else {
if (!document.hidden) {
sse_fallback();
sse_fallback_interval = setInterval(sse_fallback, updateInterval);
}
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
clearInterval(sse_fallback_interval);
}
else {
sse_offset = 0;
sse_bs_init();
sse_fallback_interval = setInterval(sse_fallback, updateInterval);
}
}, false);
}
document.querySelectorAll('.notification-link').forEach(function (element) {
element.addEventListener('click', function (element) {
sse_bs_notifications(element, true, false);
});
});
document.querySelectorAll('.notification-filter').forEach(function (element) {
element.addEventListener('keypress', function(e) {
if (e.which == 13) { // Enter key
this.blur();
sse_offset = 0;
// Clear the content of the menu
document.getElementById("nav-" + sse_type + "-menu").innerHTML = '';
// Show the loading element
document.getElementById("nav-" + sse_type + "-loading").style.display = 'block';
// Get the value from the input element
var cn_val = document.getElementById('cn-' + sse_type + '-input') ? document.getElementById('cn-' + sse_type + '-input').value.toString().toLowerCase() : '';
// Send a GET request using the Fetch API
fetch('/sse_bs/' + sse_type + '/' + sse_offset + '?nquery=' + encodeURIComponent(cn_val))
.then(response => response.json())
.then(obj => {
console.log('sse: bootstraping ' + sse_type);
console.log(obj);
sse_bs_active = false;
sse_partial_result = true;
sse_offset = obj[sse_type].offset;
if (sse_offset < 0) {
document.getElementById("nav-" + sse_type + "-loading").style.display = 'none';
}
sse_handleNotifications(obj, true, false);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
});
});
document.querySelectorAll('.notifications-textinput-clear').forEach(function (element) {
element.addEventListener('click', function(e) {
if (!sse_partial_result) return;
// Clear the content of the menu
document.getElementById("nav-" + sse_type + "-menu").innerHTML = '';
// Show the loading element
document.getElementById("nav-" + sse_type + "-loading").style.display = 'block';
// Send a GET request using the Fetch API
fetch('/sse_bs/' + sse_type)
.then(response => response.json())
.then(obj => {
console.log('sse: bootstraping ' + sse_type);
console.log(obj);
sse_bs_active = false;
sse_partial_result = false;
sse_offset = obj[sse_type].offset;
if (sse_offset < 0) {
document.getElementById("nav-" + sse_type + "-loading").style.display = 'none';
}
sse_handleNotifications(obj, true, false);
})
.catch(error => {
console.error('Error fetching data:', error);
});
});
});
document.querySelectorAll('.notification-content').forEach(function(element) {
element.addEventListener('scroll', function() {
if (this.scrollTop > this.scrollHeight - this.clientHeight - (this.scrollHeight / 7)) {
sse_bs_notifications(sse_type, false, true);
}
});
});
{{foreach $notifications as $notification}}
{{if $notification.filter}}
document.querySelectorAll('#tt-{{$notification.type}}-only').forEach(function (element) {
element.addEventListener('click', function(e) {
let element = e.target.closest('div');
let menu = document.querySelector('#nav-{{$notification.type}}-menu');
let notifications = menu.querySelectorAll('.notification[data-thread_top="false"]');
// Function to check if an element is visible
function isVisible(el) {
return el.offsetWidth > 0 && el.offsetHeight > 0;
}
if (element.classList.contains('active') && element.classList.contains('sticky-top')) {
notifications.forEach(function(notification) {
notification.classList.remove('tt-filter-active');
});
element.classList.remove('active', 'sticky-top');
} else {
notifications.forEach(function(notification) {
notification.classList.add('tt-filter-active');
});
element.classList.add('active', 'sticky-top');
// Count the visible notifications
let visibleNotifications = Array.from(menu.querySelectorAll('.notification')).filter(isVisible).length;
// Load more notifications if the visible count is low
if (sse_type && sse_offset !== -1 && visibleNotifications < 15) {
sse_bs_notifications(sse_type, false, true);
}
}
});
});
document.querySelectorAll('#cn-{{$notification.type}}-input-clear').forEach(function (element) {
element.addEventListener('click', function(e) {
let input = document.getElementById('cn-{{$notification.type}}-input');
input.value = '';
// Remove 'active' and 'sticky-top' classes to the 'only' element
let onlyElement = document.getElementById('cn-{{$notification.type}}-only');
onlyElement.classList.remove('active', 'sticky-top');
// Add 'd-none' class from the clear button
let clearButton = document.getElementById('cn-{{$notification.type}}-input-clear');
clearButton.classList.add('d-none');
// Remove the 'cn-filter-active' class from all notifications
let notifications = document.querySelectorAll("#nav-{{$notification.type}}-menu .notification");
notifications.forEach(function(notification) {
notification.classList.remove('cn-filter-active');
});
});
});
document.querySelectorAll('#cn-{{$notification.type}}-input').forEach(function (element) {
element.addEventListener('input', function(e) {
let input = e.target;
let val = input.value.toString().toLowerCase();
// Check if there is input value
if (val) {
// Remove '%' if it's at the beginning of the input value
val = val.indexOf('%') === 0 ? val.substring(1) : val;
// Add 'active' and 'sticky-top' classes to the 'only' element
let onlyElement = document.getElementById('cn-{{$notification.type}}-only');
onlyElement.classList.add('active', 'sticky-top');
// Remove 'd-none' class from the clear button
let clearButton = document.getElementById('cn-{{$notification.type}}-input-clear');
clearButton.classList.remove('d-none');
} else {
// Remove 'active' and 'sticky-top' classes from the 'only' element
let onlyElement = document.getElementById('cn-{{$notification.type}}-only');
onlyElement.classList.remove('active', 'sticky-top');
// Add 'd-none' class to the clear button
let clearButton = document.getElementById('cn-{{$notification.type}}-input-clear');
clearButton.classList.add('d-none');
}
// Loop through each notification and apply filter logic
let notifications = document.querySelectorAll("#nav-{{$notification.type}}-menu .notification");
notifications.forEach(function(el) {
let cn = el.dataset.contact_name.toString().toLowerCase();
let ca = el.dataset.contact_addr.toString().toLowerCase();
// Check if the contact name or address matches the input value
if (cn.indexOf(val) === -1 && ca.indexOf(val) === -1) {
el.classList.add('cn-filter-active');
} else {
el.classList.remove('cn-filter-active');
}
});
});
});
{{/if}}
{{/foreach}}
});
document.addEventListener('hz:sse_setNotificationsStatus', function(e) {
sse_setNotificationsStatus(e.detail);
});
document.addEventListener('hz:sse_bs_init', function() {
sse_bs_init();
});
document.addEventListener('hz:sse_bs_counts', function() {
sse_bs_counts();
});
function sse_bs_init() {
// Check if 'notification_open' exists in sessionStorage or if sse_type is defined
if (sessionStorage.getItem('notification_open') !== null || typeof sse_type !== 'undefined') {
if (typeof sse_type === 'undefined') {
sse_type = sessionStorage.getItem('notification_open');
}
// Add the 'show' class to the appropriate element
let subNav = document.getElementById("nav-" + sse_type + "-sub");
if (subNav) {
subNav.classList.add('show');
}
// Call the sse_bs_notifications function
sse_bs_notifications(sse_type, true, false);
} else {
// Call the sse_bs_counts function if conditions are not met
sse_bs_counts();
}
}
function sse_bs_counts() {
if (sse_bs_active || sse_sys_only) {
return;
}
sse_bs_active = true;
// Use the fetch API to send the POST request with the data
fetch('/sse_bs', {
method: 'POST',
body: new URLSearchParams({sse_rmids: sse_rmids})
})
.then(response => response.json()) // Parse the JSON response
.then(obj => {
console.log(obj);
sse_bs_active = false;
sse_rmids = [];
sse_handleNotifications(obj, true, false);
})
.catch(error => {
console.error('Error:', error);
sse_bs_active = false;
});
}
function sse_bs_notifications(e, replace, followup) {
if (sse_bs_active || sse_sys_only) {
return;
}
let manual = false;
if (typeof replace === 'undefined') {
replace = e.data.replace;
}
if (typeof followup === 'undefined') {
followup = e.data.followup;
}
if (typeof e === 'string') {
sse_type = e;
} else {
manual = true;
sse_offset = 0;
sse_type = e.target.dataset.sse_type;
}
if (typeof sse_type === 'undefined') {
return;
}
if (followup || !manual || !document.getElementById('notification-link-' + sse_type).classList.contains('collapsed')) {
if (sse_offset >= 0) {
document.getElementById("nav-" + sse_type + "-loading").style.display = 'block';
}
sessionStorage.setItem('notification_open', sse_type);
if (sse_offset !== -1 || replace) {
let cn_val = (document.getElementById('cn-' + sse_type + '-input') && sse_partial_result)
? document.getElementById('cn-' + sse_type + '-input').value.toString().toLowerCase()
: '';
document.getElementById("nav-" + sse_type + "-loading").style.display = 'block';
sse_bs_active = true;
// Send POST request using fetch API
fetch('/sse_bs/' + sse_type + '/' + sse_offset, {
method: 'POST',
body: new URLSearchParams({
sse_rmids: sse_rmids,
nquery: encodeURIComponent(cn_val)
})
})
.then(response => response.json()) // Parse the JSON response
.then(obj => {
console.log('sse: bootstraping ' + sse_type);
console.log(obj);
sse_bs_active = false;
sse_rmids = [];
document.getElementById("nav-" + sse_type + "-loading").style.display = 'none';
sse_offset = obj[sse_type].offset;
sse_handleNotifications(obj, replace, followup);
})
.catch(error => {
console.error('Error:', error);
sse_bs_active = false;
});
} else {
document.getElementById("nav-" + sse_type + "-loading").style.display = 'none';
}
} else {
sessionStorage.removeItem('notification_open');
}
}
function sse_handleNotifications(obj, replace, followup) {
// Notice and info notifications
if (obj.notice) {
obj.notice.notifications.forEach(notification => {
toast(notification, 'danger');
});
}
if (obj.info) {
obj.info.notifications.forEach(notification => {
toast(notification, 'info');
});
}
if (sse_sys_only) {
return;
}
let primary_notifications = ['dm', 'home', 'intros', 'register', 'notify', 'files'];
let secondary_notifications = ['network', 'forums', 'all_events', 'pubs'];
let all_notifications = [...primary_notifications, ...secondary_notifications];
all_notifications.forEach(type => {
if (typeof obj[type] === 'undefined') {
return;
}
let count = Number(obj[type].count);
// Show notifications and update count
let updateElement = document.querySelector('.' + type + '-update');
let buttonElement = document.querySelector('.' + type + '-button');
let subElement = document.getElementById('nav-' + type + '-sub');
if (count) {
if (buttonElement) buttonElement.style.display = 'block'; // Fade-in effect replaced by display block
if (replace || followup) {
updateElement.textContent = count >= 100 ? '99+' : count;
} else {
count = count + Number(updateElement.textContent.replace(/\++$/, ''));
updateElement.textContent = count >= 100 ? '99+' : count;
}
} else {
if (updateElement) updateElement.textContent = '0';
if (subElement) subElement.classList.remove('show');
if (buttonElement) {
buttonElement.style.display = 'none'; // Fade-out effect replaced by display none
sse_setNotificationsStatus();
}
}
if (obj[type].notifications.length) {
sse_handleNotificationsItems(type, obj[type].notifications, replace, followup);
}
});
sse_setNotificationsStatus();
// Load more notifications if visible notifications count becomes low
if (sse_type && sse_offset !== -1) {
let menu = document.getElementById('nav-' + sse_type + '-menu');
if (menu && menu.children.length < 15) {
sse_bs_notifications(sse_type, false, true);
}
}
}
function sse_handleNotificationsItems(notifyType, data, replace, followup) {
// Get the template, adjust based on the notification type
let notifications_tpl = (notifyType === 'forums')
? decodeURIComponent(document.querySelector("#nav-notifications-forums-template[rel=template]").innerHTML.replace('data-src', 'src'))
: decodeURIComponent(document.querySelector("#nav-notifications-template[rel=template]").innerHTML.replace('data-src', 'src'));
let notify_menu = document.getElementById("nav-" + notifyType + "-menu");
let notify_loading = document.getElementById("nav-" + notifyType + "-loading");
let notify_count = document.getElementsByClassName(notifyType + "-update");
if (replace && !followup) {
notify_menu.innerHTML = ''; // Clear menu
notify_loading.style.display = 'none'; // Hide loading
}
data.forEach(notification => {
// Special handling for network notifications
if (!replace && !followup && notification.thread_top && notifyType === 'network') {
document.dispatchEvent(new CustomEvent('hz:handleNetworkNotificationsItems', { detail: notification }));
}
// Prepare HTML using the template
let html = notifications_tpl.format(
notification.notify_link,
notification.photo,
notification.name,
notification.addr,
notification.message,
notification.when,
notification.hclass,
notification.b64mid,
notification.notify_id,
notification.thread_top,
notification.unseen,
notification.private_forum,
encodeURIComponent(notification.mids),
notification.body
);
// Append the new notification HTML to the menu
notify_menu.insertAdjacentHTML('beforeend', html);
});
// Sort notifications by date
if (!replace && !followup) {
let notifications = Array.from(notify_menu.getElementsByClassName('notification'));
notifications.sort((a, b) => {
let dateA = new Date(a.dataset.when);
let dateB = new Date(b.dataset.when);
return dateA > dateB ? -1 : dateA < dateB ? 1 : 0;
});
notifications.forEach(notification => notify_menu.appendChild(notification));
}
// Filter thread_top notifications if the filter is active
let filterThreadTop = document.getElementById('tt-' + notifyType + '-only');
if (filterThreadTop && filterThreadTop.classList.contains('active')) {
let notifications = notify_menu.querySelectorAll('[data-thread_top="false"]');
notifications.forEach(notification => notification.classList.add('tt-filter-active'));
}
// Filter notifications based on the input field
let filterInput = document.getElementById('cn-' + notifyType + '-input');
if (filterInput) {
let filter = filterInput.value.toString().toLowerCase();
if (filter) {
if (filter.indexOf('%') === 0) filter = filter.substring(1); // Remove the percent if it exists
let notifications = notify_menu.querySelectorAll('.notification');
notifications.forEach(notification => {
let cn = notification.dataset.contact_name.toString().toLowerCase();
let ca = notification.dataset.contact_addr.toString().toLowerCase();
if (cn.indexOf(filter) === -1 && ca.indexOf(filter) === -1) {
notification.classList.add('cn-filter-active');
} else {
notification.classList.remove('cn-filter-active');
}
});
}
}
// Update relative time for notifications
updateRelativeTime('.autotime-narrow');
}
function sse_updateNotifications(type, mid) {
// Skip processing if the type is 'notify' and the conditions don't match
if (type === 'notify' && (mid !== bParam_mid || sse_type !== 'notify')) {
return true;
}
// Find the notification element based on its 'data-b64mid' attribute
let notification = document.querySelector(`#nav-${type}-menu .notification[data-b64mid='${mid}']`);
if (notification) {
notification.remove();
}
}
function sse_setNotificationsStatus(data) {
let primary_notifications = ['dm', 'home', 'intros', 'register', 'notify', 'files'];
let secondary_notifications = ['network', 'forums', 'all_events', 'pubs'];
let all_notifications = primary_notifications.concat(secondary_notifications);
let primary_available = false;
let any_available = false;
// Loop through all notifications and check their visibility
all_notifications.forEach(function (type) {
let button = document.querySelector(`.${type}-button`);
if (button && getComputedStyle(button).display === 'block') {
any_available = true;
if (primary_notifications.indexOf(type) > -1) {
primary_available = true;
}
}
});
// Update notification button icon based on the primary notification availability
let notificationIcon = document.querySelector('.notifications-btn-icon');
if (notificationIcon) {
let iconClass = primary_available ? 'bi-exclamation-triangle' : 'bi-exclamation-circle';
let iconToRemove = primary_available ? 'bi-exclamation-circle' : 'bi-exclamation-triangle';
notificationIcon.classList.replace(iconToRemove, iconClass);
}
// Update visibility of notification button and sections
let notificationsBtn = document.querySelectorAll('.notifications-btn');
let noNotifications = document.querySelector('#no_notifications');
let notifications = document.querySelector('#notifications');
let navbarCollapse = document.querySelector('#navbar-collapse-1');
if (any_available) {
notificationsBtn.forEach(btn => {
btn.style.opacity = 1;
});
noNotifications.style.display = 'none';
notifications.style.display = 'block';
} else {
if (notificationsBtn) {
notificationsBtn.forEach(btn => {
btn.style.opacity = 0.5;
});
}
if (navbarCollapse) navbarCollapse.classList.remove('show');
noNotifications.style.display = 'block';
notifications.style.display = 'none';
}
// Handle specific notifications if 'data' is provided
if (typeof data !== 'undefined') {
data.forEach(function (nmid) {
sse_rmids.push(nmid);
// Handle regular notifications
let notification = document.querySelector(`.notification[data-b64mid='${nmid}']`);
if (notification) {
let parentId = notification.parentElement.id.split('-')[1];
sse_updateNotifications(parentId, nmid);
}
// Special handling for forum notifications
let forumNotifications = document.querySelectorAll('.notification-forum');
forumNotifications.forEach(function (forumNotification) {
let fmids = decodeURIComponent(forumNotification.dataset.b64mids);
let parentId = forumNotification.parentElement.id.split('-')[1];
if (fmids.indexOf(nmid) > -1) {
let updateElem = document.querySelector(`.${parentId}-update`);
let fcount = Number(updateElem.innerText);
fcount--;
updateElem.innerText = fcount;
if (fcount < 1) {
let button = document.querySelector(`.${parentId}-button`);
button.style.display = 'none';
let subMenu = document.querySelector(`#nav-${parentId}-sub`);
if (subMenu) subMenu.classList.remove('show');
}
let countElem = forumNotification.querySelector('.bg-secondary');
let count = Number(countElem.innerText);
count--;
countElem.innerText = count;
if (count < 1) {
forumNotification.remove();
}
}
});
});
}
}
function sse_fallback() {
fetch('/sse')
.then(response => response.json())
.then(obj => {
if (!obj) return;
console.log('sse fallback');
console.log(obj);
sse_handleNotifications(obj, false, false);
})
.catch(error => {
console.error('Error fetching SSE data:', error);
});
}
</script>
{{if !$sys_only}}
<div id="notifications_wrapper" class="mb-4">
<div id="no_notifications" class="d-xl-none">
{{$no_notifications}}<span class="jumping-dots"><span class="dot-1">.</span><span class="dot-2">.</span><span class="dot-3">.</span></span>
</div>
<div id="nav-notifications-template" rel="template" class="d-none">
<a class="list-group-item list-group-item-action notification {6}" href="{0}" title="{13}" data-b64mid="{7}" data-notify_id="{8}" data-thread_top="{9}" data-contact_name="{2}" data-contact_addr="{3}" data-when="{5}">
<img data-src="{1}" loading="lazy" class="rounded float-start me-2 menu-img-2">
<div class="text-nowrap">
<div class="d-flex justify-content-between align-items-center lh-sm">
<div class="text-truncate pe-1">
<strong title="{2} - {3}">{2}</strong>
</div>
<small class="autotime-narrow text-body-secondary" title="{5}"></small>
</div>
<div class="text-truncate">{4}</div>
</div>
</a>
</div>
<div id="nav-notifications-forums-template" rel="template" class="d-none">
<a class="list-group-item list-group-item-action justify-content-between align-items-center d-flex notification notification-forum" href="{0}" title="{4} - {3}" data-b64mid="{7}" data-notify_id="{8}" data-thread_top="{9}" data-contact_name="{2}" data-contact_addr="{3}" data-b64mids='{12}'>
<div>
<img class="menu-img-1" data-src="{1}" loading="lazy">
<span>{2}</span>
</div>
<span class="badge bg-secondary">{10}</span>
</a>
</div>
<div id="notifications" class="border border-top-0 rounded navbar-nav collapse">
{{foreach $notifications as $notification}}
<div class="rounded-top rounded-bottom border border-start-0 border-end-0 border-bottom-0 list-group list-group-flush collapse {{$notification.type}}-button">
<a id="notification-link-{{$notification.type}}" class="collapsed list-group-item justify-content-between align-items-center d-flex fakelink stretched-link notification-link" href="#" title="{{$notification.title}}" data-bs-target="#nav-{{$notification.type}}-sub" data-bs-toggle="collapse" data-sse_type="{{$notification.type}}">
<div>
<i class="bi bi-{{$notification.icon}} generic-icons-nav"></i>
{{$notification.label}}
</div>
<span class="badge bg-{{$notification.severity}} {{$notification.type}}-update"></span>
</a>
</div>
<div id="nav-{{$notification.type}}-sub" class="rounded-bottom border border-start-0 border-end-0 border-bottom-0 list-group list-group-flush collapse notification-content" data-bs-parent="#notifications" data-sse_type="{{$notification.type}}">
{{if $notification.viewall}}
<a class="list-group-item list-group-item-action text-decoration-none" id="nav-{{$notification.type}}-see-all" href="{{$notification.viewall.url}}">
<i class="bi bi-box-arrow-up-right generic-icons-nav"></i> {{$notification.viewall.label}}
</a>
{{/if}}
{{if $notification.markall}}
<div class="list-group-item list-group-item-action cursor-pointer" id="nav-{{$notification.type}}-mark-all" onclick="markRead('{{$notification.type}}'); return false;">
<i class="bi bi-check-circle generic-icons-nav"></i> {{$notification.markall.label}}
</div>
{{/if}}
{{if $notification.filter}}
{{if $notification.filter.posts_label}}
<div class="list-group-item list-group-item-action cursor-pointer" id="tt-{{$notification.type}}-only">
<i class="bi bi-funnel generic-icons-nav"></i> {{$notification.filter.posts_label}}
</div>
{{/if}}
{{if $notification.filter.name_label}}
<div class="list-group-item clearfix notifications-textinput" id="cn-{{$notification.type}}-only">
<div class="text-muted notifications-textinput-filter"><i class="bi bi-filter"></i></div>
<input id="cn-{{$notification.type}}-input" type="text" class="notification-filter form-control form-control-sm" placeholder="{{$notification.filter.name_label}}">
<div id="cn-{{$notification.type}}-input-clear" class="text-muted notifications-textinput-clear d-none"><i class="bi bi-x-lg"></i></div>
</div>
{{/if}}
{{/if}}
<div id="nav-{{$notification.type}}-menu" class="list-group list-group-flush"></div>
<div id="nav-{{$notification.type}}-loading" class="list-group-item" style="display: none;">
{{$loading}}<span class="jumping-dots"><span class="dot-1">.</span><span class="dot-2">.</span><span class="dot-3">.</span></span>
</div>
</div>
{{/foreach}}
</div>
</div>
{{/if}}