233 lines
7.0 KiB
JavaScript
233 lines
7.0 KiB
JavaScript
|
|
/**
|
||
|
|
* @fileoverview Notification List Component for Rantii
|
||
|
|
* @author retoor <retoor@molodetz.nl>
|
||
|
|
* @description Displays user notifications and mentions
|
||
|
|
* @keywords notification, list, alerts, mentions, updates
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { BaseComponent } from './base-component.js';
|
||
|
|
import { formatRelativeTime } from '../utils/date.js';
|
||
|
|
|
||
|
|
class NotificationList extends BaseComponent {
|
||
|
|
init() {
|
||
|
|
this.notifications = [];
|
||
|
|
this.isLoading = false;
|
||
|
|
this.unreadCount = 0;
|
||
|
|
this.render();
|
||
|
|
this.bindEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
async load() {
|
||
|
|
if (!this.isLoggedIn()) {
|
||
|
|
this.render();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.isLoading = true;
|
||
|
|
this.render();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await this.getApi()?.getNotifications();
|
||
|
|
if (result?.success) {
|
||
|
|
this.notifications = result.notifications || [];
|
||
|
|
this.unreadCount = result.unread?.total || 0;
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
this.notifications = [];
|
||
|
|
} finally {
|
||
|
|
this.isLoading = false;
|
||
|
|
this.render();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
this.addClass('notification-list');
|
||
|
|
|
||
|
|
if (!this.isLoggedIn()) {
|
||
|
|
this.setHtml(`
|
||
|
|
<div class="notification-auth">
|
||
|
|
<p>Sign in to view notifications</p>
|
||
|
|
<button class="btn btn-primary login-btn">Sign In</button>
|
||
|
|
</div>
|
||
|
|
`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.isLoading) {
|
||
|
|
this.setHtml(`
|
||
|
|
<div class="notification-loading">
|
||
|
|
<loading-spinner text="Loading notifications..."></loading-spinner>
|
||
|
|
</div>
|
||
|
|
`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.notifications.length === 0) {
|
||
|
|
this.setHtml(`
|
||
|
|
<div class="notification-empty">
|
||
|
|
<svg viewBox="0 0 24 24" width="48" height="48">
|
||
|
|
<path fill="currentColor" d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"/>
|
||
|
|
</svg>
|
||
|
|
<p>No notifications</p>
|
||
|
|
</div>
|
||
|
|
`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.setHtml(`
|
||
|
|
<header class="notification-header">
|
||
|
|
<h2>Notifications</h2>
|
||
|
|
${this.unreadCount > 0 ? `
|
||
|
|
<button class="clear-btn">Mark all read</button>
|
||
|
|
` : ''}
|
||
|
|
</header>
|
||
|
|
<div class="notification-items">
|
||
|
|
${this.notifications.map(notif => `
|
||
|
|
<notification-item
|
||
|
|
data-notif='${JSON.stringify(notif).replace(/'/g, ''')}'>
|
||
|
|
</notification-item>
|
||
|
|
`).join('')}
|
||
|
|
</div>
|
||
|
|
`);
|
||
|
|
|
||
|
|
this.initNotificationItems();
|
||
|
|
}
|
||
|
|
|
||
|
|
initNotificationItems() {
|
||
|
|
const items = this.$$('notification-item');
|
||
|
|
items.forEach(item => {
|
||
|
|
const notifData = item.dataset.notif;
|
||
|
|
if (notifData) {
|
||
|
|
try {
|
||
|
|
const notif = JSON.parse(notifData.replace(/'/g, "'"));
|
||
|
|
item.setNotification(notif);
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
bindEvents() {
|
||
|
|
this.on(this, 'click', this.handleClick);
|
||
|
|
}
|
||
|
|
|
||
|
|
handleClick(e) {
|
||
|
|
const loginBtn = e.target.closest('.login-btn');
|
||
|
|
const clearBtn = e.target.closest('.clear-btn');
|
||
|
|
|
||
|
|
if (loginBtn) {
|
||
|
|
this.getRouter()?.goToLogin();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (clearBtn) {
|
||
|
|
this.clearNotifications();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async clearNotifications() {
|
||
|
|
try {
|
||
|
|
await this.getApi()?.clearNotifications();
|
||
|
|
this.unreadCount = 0;
|
||
|
|
this.emit('notifications-cleared');
|
||
|
|
await this.load();
|
||
|
|
} catch (error) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
onConnected() {
|
||
|
|
this.load();
|
||
|
|
}
|
||
|
|
|
||
|
|
onDisconnected() {
|
||
|
|
this.isLoading = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
getUnreadCount() {
|
||
|
|
return this.unreadCount;
|
||
|
|
}
|
||
|
|
|
||
|
|
refresh() {
|
||
|
|
this.load();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
customElements.define('notification-list', NotificationList);
|
||
|
|
|
||
|
|
class NotificationItem extends BaseComponent {
|
||
|
|
init() {
|
||
|
|
this.notifData = null;
|
||
|
|
this.render();
|
||
|
|
this.bindEvents();
|
||
|
|
}
|
||
|
|
|
||
|
|
setNotification(notif) {
|
||
|
|
this.notifData = notif;
|
||
|
|
this.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
render() {
|
||
|
|
if (!this.notifData) {
|
||
|
|
this.setHtml('');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const notif = this.notifData;
|
||
|
|
const isUnread = notif.read === 0;
|
||
|
|
const typeLabel = this.getTypeLabel(notif.type);
|
||
|
|
const username = notif.username || notif.user_username || notif.name || 'Someone';
|
||
|
|
|
||
|
|
this.addClass('notification-item');
|
||
|
|
if (isUnread) {
|
||
|
|
this.addClass('unread');
|
||
|
|
}
|
||
|
|
|
||
|
|
this.setHtml(`
|
||
|
|
<div class="notif-content">
|
||
|
|
<div class="notif-icon">${this.getTypeIcon(notif.type)}</div>
|
||
|
|
<div class="notif-body">
|
||
|
|
<span class="notif-username">${username}</span>
|
||
|
|
<span class="notif-action">${typeLabel}</span>
|
||
|
|
</div>
|
||
|
|
<time class="notif-time">${formatRelativeTime(notif.created_time)}</time>
|
||
|
|
</div>
|
||
|
|
`);
|
||
|
|
}
|
||
|
|
|
||
|
|
getTypeLabel(type) {
|
||
|
|
const labels = {
|
||
|
|
'comment_mention': 'mentioned you',
|
||
|
|
'comment_content': 'commented on your rant',
|
||
|
|
'comment_vote': 'upvoted your comment',
|
||
|
|
'rant_vote': 'upvoted your rant',
|
||
|
|
'comment_discuss': 'replied to a discussion'
|
||
|
|
};
|
||
|
|
return labels[type] || 'interacted';
|
||
|
|
}
|
||
|
|
|
||
|
|
getTypeIcon(type) {
|
||
|
|
if (type.includes('mention')) {
|
||
|
|
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10h5v-2h-5c-4.34 0-8-3.66-8-8s3.66-8 8-8 8 3.66 8 8v1.43c0 .79-.71 1.57-1.5 1.57s-1.5-.78-1.5-1.57V12c0-2.76-2.24-5-5-5s-5 2.24-5 5 2.24 5 5 5c1.38 0 2.64-.56 3.54-1.47.65.89 1.77 1.47 2.96 1.47 1.97 0 3.5-1.6 3.5-3.57V12c0-5.52-4.48-10-10-10zm0 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/></svg>`;
|
||
|
|
}
|
||
|
|
if (type.includes('vote')) {
|
||
|
|
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>`;
|
||
|
|
}
|
||
|
|
return `<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/></svg>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
bindEvents() {
|
||
|
|
this.on(this, 'click', this.handleClick);
|
||
|
|
}
|
||
|
|
|
||
|
|
handleClick() {
|
||
|
|
if (this.notifData?.rant_id) {
|
||
|
|
this.getRouter()?.goToRant(
|
||
|
|
this.notifData.rant_id,
|
||
|
|
this.notifData.comment_id
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
customElements.define('notification-item', NotificationItem);
|
||
|
|
|
||
|
|
export { NotificationList, NotificationItem };
|