feat: Initial release v0.1.0

WP FediStream - Stream music over ActivityPub

Features:
- Custom post types: Artist, Album, Track, Playlist
- Custom taxonomies: Genre, Mood, License
- User roles: Artist, Label
- Admin dashboard with statistics
- Frontend templates and shortcodes
- Audio player with queue management
- ActivityPub integration with actor support
- WooCommerce product types for albums/tracks
- User library with favorites and history
- Notification system (in-app and email)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 23:23:05 +01:00
commit 4a5d7b9f4d
91 changed files with 22750 additions and 0 deletions

353
assets/js/notifications.js Normal file
View File

@@ -0,0 +1,353 @@
/**
* FediStream Notifications JavaScript
*
* @package WP_FediStream
*/
( function( $ ) {
'use strict';
const Notifications = {
unreadCount: 0,
isOpen: false,
pollTimer: null,
init: function() {
this.createDropdown();
this.bindEvents();
this.loadNotifications();
this.startPolling();
},
createDropdown: function() {
const dropdown = `
<div class="fedistream-notifications-dropdown" style="display: none;">
<div class="notifications-header">
<h4>${fedistreamNotifications.i18n.viewAll || 'Notifications'}</h4>
<button class="mark-all-read" title="${fedistreamNotifications.i18n.markAllRead}">
<span class="dashicons dashicons-yes-alt"></span>
</button>
</div>
<div class="notifications-list">
<div class="loading">${fedistreamNotifications.i18n.loading || 'Loading...'}</div>
</div>
<div class="notifications-footer">
<a href="#" class="view-all">${fedistreamNotifications.i18n.viewAll}</a>
</div>
</div>
`;
$( '.fedistream-notifications-menu' ).append( dropdown );
},
bindEvents: function() {
const self = this;
// Toggle dropdown.
$( document ).on( 'click', '#wp-admin-bar-fedistream-notifications > a', function( e ) {
e.preventDefault();
self.toggleDropdown();
} );
// Close dropdown when clicking outside.
$( document ).on( 'click', function( e ) {
if ( ! $( e.target ).closest( '.fedistream-notifications-menu' ).length ) {
self.closeDropdown();
}
} );
// Mark all as read.
$( document ).on( 'click', '.fedistream-notifications-dropdown .mark-all-read', function( e ) {
e.preventDefault();
e.stopPropagation();
self.markAllRead();
} );
// Mark single as read.
$( document ).on( 'click', '.notification-item', function() {
const id = $( this ).data( 'id' );
const isRead = $( this ).hasClass( 'is-read' );
if ( ! isRead ) {
self.markRead( id, $( this ) );
}
} );
// Delete notification.
$( document ).on( 'click', '.notification-item .delete-btn', function( e ) {
e.preventDefault();
e.stopPropagation();
const item = $( this ).closest( '.notification-item' );
const id = item.data( 'id' );
self.deleteNotification( id, item );
} );
},
toggleDropdown: function() {
if ( this.isOpen ) {
this.closeDropdown();
} else {
this.openDropdown();
}
},
openDropdown: function() {
$( '.fedistream-notifications-dropdown' ).show();
this.isOpen = true;
this.loadNotifications();
},
closeDropdown: function() {
$( '.fedistream-notifications-dropdown' ).hide();
this.isOpen = false;
},
loadNotifications: function() {
const self = this;
$.ajax( {
url: fedistreamNotifications.ajaxUrl,
type: 'POST',
data: {
action: 'fedistream_get_notifications',
nonce: fedistreamNotifications.nonce,
limit: 10
},
success: function( response ) {
if ( response.success ) {
self.renderNotifications( response.data.notifications );
self.updateUnreadCount( response.data.unread_count );
}
},
error: function() {
$( '.notifications-list' ).html(
'<p class="error">' + fedistreamNotifications.i18n.error + '</p>'
);
}
} );
},
renderNotifications: function( notifications ) {
const container = $( '.notifications-list' );
container.empty();
if ( ! notifications || notifications.length === 0 ) {
container.html(
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
);
return;
}
notifications.forEach( function( notification ) {
container.append( this.createNotificationItem( notification ) );
}, this );
},
createNotificationItem: function( notification ) {
const isRead = notification.is_read ? 'is-read' : '';
const icon = this.getNotificationIcon( notification.type );
const time = this.formatTime( notification.created_at );
const link = this.getNotificationLink( notification );
return `
<div class="notification-item ${isRead}" data-id="${notification.id}">
<div class="notification-icon">
<span class="dashicons dashicons-${icon}"></span>
</div>
<div class="notification-content">
<div class="notification-title">${this.escapeHtml( notification.title )}</div>
<div class="notification-message">${this.escapeHtml( notification.message )}</div>
<div class="notification-time">${time}</div>
</div>
<div class="notification-actions">
<button class="delete-btn" title="Delete">
<span class="dashicons dashicons-no-alt"></span>
</button>
</div>
${link ? `<a href="${link}" class="notification-link"></a>` : ''}
</div>
`;
},
getNotificationIcon: function( type ) {
const icons = {
'new_release': 'album',
'new_follower': 'admin-users',
'fediverse_like': 'heart',
'fediverse_boost': 'megaphone',
'playlist_added': 'playlist-audio',
'purchase': 'cart',
'system': 'info'
};
return icons[ type ] || 'bell';
},
getNotificationLink: function( notification ) {
const data = notification.data || {};
if ( data.album_url ) {
return data.album_url;
}
if ( data.track_url ) {
return data.track_url;
}
if ( data.artist_url ) {
return data.artist_url;
}
return null;
},
markRead: function( notificationId, element ) {
const self = this;
$.ajax( {
url: fedistreamNotifications.ajaxUrl,
type: 'POST',
data: {
action: 'fedistream_mark_notification_read',
nonce: fedistreamNotifications.nonce,
notification_id: notificationId
},
success: function( response ) {
if ( response.success ) {
element.addClass( 'is-read' );
self.updateUnreadCount( response.data.unread_count );
}
}
} );
},
markAllRead: function() {
const self = this;
$.ajax( {
url: fedistreamNotifications.ajaxUrl,
type: 'POST',
data: {
action: 'fedistream_mark_all_notifications_read',
nonce: fedistreamNotifications.nonce
},
success: function( response ) {
if ( response.success ) {
$( '.notification-item' ).addClass( 'is-read' );
self.updateUnreadCount( 0 );
}
}
} );
},
deleteNotification: function( notificationId, element ) {
const self = this;
$.ajax( {
url: fedistreamNotifications.ajaxUrl,
type: 'POST',
data: {
action: 'fedistream_delete_notification',
nonce: fedistreamNotifications.nonce,
notification_id: notificationId
},
success: function( response ) {
if ( response.success ) {
element.fadeOut( 200, function() {
$( this ).remove();
// Check if list is empty.
if ( $( '.notification-item' ).length === 0 ) {
$( '.notifications-list' ).html(
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
);
}
} );
self.updateUnreadCount( response.data.unread_count );
}
}
} );
},
updateUnreadCount: function( count ) {
this.unreadCount = count;
const badge = $( '.fedistream-notification-count' );
if ( count > 0 ) {
if ( badge.length ) {
badge.text( count );
} else {
$( '#wp-admin-bar-fedistream-notifications .ab-icon' ).after(
'<span class="fedistream-notification-count">' + count + '</span>'
);
}
} else {
badge.remove();
}
},
startPolling: function() {
const self = this;
const interval = fedistreamNotifications.pollInterval || 60000;
this.pollTimer = setInterval( function() {
if ( ! self.isOpen ) {
self.checkForNewNotifications();
}
}, interval );
},
checkForNewNotifications: function() {
const self = this;
$.ajax( {
url: fedistreamNotifications.ajaxUrl,
type: 'POST',
data: {
action: 'fedistream_get_notifications',
nonce: fedistreamNotifications.nonce,
unread_only: true,
limit: 1
},
success: function( response ) {
if ( response.success ) {
self.updateUnreadCount( response.data.unread_count );
}
}
} );
},
formatTime: function( dateString ) {
const date = new Date( dateString );
const now = new Date();
const diff = Math.floor( ( now - date ) / 1000 );
if ( diff < 60 ) {
return fedistreamNotifications.i18n.justNow || 'Just now';
} else if ( diff < 3600 ) {
const mins = Math.floor( diff / 60 );
return mins + 'm ago';
} else if ( diff < 86400 ) {
const hours = Math.floor( diff / 3600 );
return hours + 'h ago';
} else if ( diff < 604800 ) {
const days = Math.floor( diff / 86400 );
return days + 'd ago';
} else {
return date.toLocaleDateString();
}
},
escapeHtml: function( text ) {
const div = document.createElement( 'div' );
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on document ready.
$( document ).ready( function() {
if ( $( '.fedistream-notifications-menu' ).length ) {
Notifications.init();
}
} );
} )( jQuery );