Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.1]

### Added

- Mercure broadcasting support via `MercureBroadcastingModule` for Server-Sent Events (SSE) real-time notifications
- Integration with `@crustum/laravel-echo-mercure` connector for Laravel Echo compatibility
- Automatic Mercure module loading when broadcaster is set to `mercure` in configuration
- Support for Mercure-specific configuration options (`mercureUrl`, `authEndpoint`)

## [1.0.0]

### Added

- Notification bell widget with dropdown and side panel display modes for displaying notifications in web interface
- Real-time broadcasting support with Pusher integration and automatic polling fallback for reliable notification delivery
- Complete JavaScript API with `NotificationManager`, `NotificationWidget`, and `NotificationRenderer` classes for managing notifications programmatically
- RESTful JSON API endpoints for notification management including mark as read, delete, and bulk operations
- Template overloading support allowing complete customization of notification rendering and UI components
- Event-driven architecture with JavaScript events for reacting to notification lifecycle events

3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"cakephp/cakephp-codesniffer": "^5.0"
},
"suggest": {
"crustum/notification-broadcasting": "Broadcasting notification channel"
"crustum/notification-broadcasting": "Broadcasting notification channel",
"crustum/mercure-broadcasting": "Mercure broadcaster driver"
},
"autoload": {
"psr-4": {
Expand Down
9 changes: 8 additions & 1 deletion config/asset_compress.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ files[] = p:Crustum/NotificationUI:js/NotificationWidget.js
files[] = p:Crustum/NotificationUI:js/index.js

; Broadcasting Bundle (includes Pusher, Echo, and our module)
; Only loaded when broadcasting is enabled
; Only loaded when broadcasting is enabled with broadcaster='pusher'
[broadcasting.js]
files[] = p:Crustum/NotificationUI:js/vendor/pusher.min.js
files[] = p:Crustum/NotificationUI:js/vendor/echo.iife.js
files[] = p:Crustum/NotificationUI:js/BroadcastingModule.js

; Mercure Broadcasting Bundle (includes Echo, MercureConnector, and Mercure module)
; Only loaded when broadcasting is enabled with broadcaster='mercure'
[mercure-broadcasting.js]
files[] = p:Crustum/NotificationUI:js/vendor/echo.iife.js
files[] = p:Crustum/NotificationUI:js/vendor/echo-mercure.js
files[] = p:Crustum/NotificationUI:js/MercureBroadcastingModule.js

[css]
paths[] = WEBROOT/css/*
cachePath = WEBROOT/_css
Expand Down
47 changes: 44 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,13 @@ Options:
<a name="real-time-broadcasting"></a>
## Real-Time Broadcasting

Enable WebSocket broadcasting for instant notification delivery:
Enable WebSocket broadcasting for instant notification delivery. Supports both Pusher and Mercure broadcasters.

### Pusher Broadcasting

> [!NOTE]
> Pusher Broadcasting requires the `crustum/notification-broadcasting` and `crustum/broadcasting` plugins. The `broadcaster` option defaults to `'pusher'`.

### Hybrid Mode (Database + Broadcasting) - Recommended

```php
<?php $authUser = $this->request->getAttribute('identity'); ?>
Expand All @@ -145,6 +149,7 @@ Enable WebSocket broadcasting for instant notification delivery:
'mode' => 'panel',
'enablePolling' => true,
'broadcasting' => [
'broadcaster' => 'pusher',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'pusherKey' => 'app-key',
Expand All @@ -155,14 +160,39 @@ Enable WebSocket broadcasting for instant notification delivery:
]) ?>
```

### Mercure Broadcasting


> [!NOTE]
> Mercure Broadcasting requires the `crustum/notification-broadcasting` and `crustum/mercure-broadcasting` plugins.


```php
<?php $authUser = $this->request->getAttribute('identity'); ?>

<?= $this->element('Crustum/NotificationUI.notifications/bell_icon', [
'mode' => 'panel',
'enablePolling' => true,
'broadcasting' => [
'broadcaster' => 'mercure',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'mercureUrl' => '/.well-known/mercure',
'authEndpoint' => '/broadcasting/auth',
],
]) ?>
```

This mode combines database persistence with real-time WebSocket delivery for the best user experience.

### Broadcasting Only (No Database)

**Pusher:**
```php
<?= $this->element('Crustum/NotificationUI.notifications/bell_icon', [
'enablePolling' => false,
'broadcasting' => [
'broadcaster' => 'pusher',
'userId' => $authUser->getIdentifier(),
'userName' => $authUser->username ?? 'User',
'pusherKey' => env('PUSHER_APP_KEY'),
Expand All @@ -173,7 +203,18 @@ This mode combines database persistence with real-time WebSocket delivery for th
]) ?>
```

> **Note:** Broadcasting requires the `cakephp/broadcasting-notification` plugin.
**Mercure:**
```php
<?= $this->element('Crustum/NotificationUI.notifications/bell_icon', [
'enablePolling' => false,
'broadcasting' => [
'broadcaster' => 'mercure',
'userId' => $authUser->getIdentifier(),
'mercureUrl' => env('MERCURE_PUBLIC_URL', '/.well-known/mercure'),
'authEndpoint' => '/broadcasting/auth',
],
]) ?>
```

<a name="notification-data-fields"></a>
## Notification Data Fields
Expand Down
45 changes: 29 additions & 16 deletions templates/element/notifications/bell_icon.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,38 @@
}

$broadcastingConfig = null;
$broadcasterType = 'pusher';
if ($broadcasting && is_array($broadcasting)) {
$broadcastingConfig = array_merge([
'userId' => null,
'userName' => 'Anonymous',
'pusherKey' => 'app-key',
'pusherHost' => '127.0.0.1',
'pusherPort' => 8080,
'pusherCluster' => 'mt1',
'channelName' => null,
], $broadcasting);
$broadcasterType = $broadcasting['broadcaster'] ?? 'pusher';

if ($broadcasterType === 'mercure') {
$broadcastingConfig = array_merge([
'broadcaster' => 'mercure',
'userId' => null,
'userName' => 'Anonymous',
'mercureUrl' => '/.well-known/mercure',
'authEndpoint' => '/broadcasting/auth',
'channelName' => null,
], $broadcasting);
} else {
$broadcastingConfig = array_merge([
'broadcaster' => 'pusher',
'userId' => null,
'userName' => 'Anonymous',
'pusherKey' => 'app-key',
'pusherHost' => '127.0.0.1',
'pusherPort' => 8080,
'pusherCluster' => 'mt1',
'channelName' => null,
], $broadcasting);
}

if (!$broadcastingConfig['channelName'] && isset($broadcastingConfig['userId'])) {
$broadcastingConfig['channelName'] = "App.Model.Entity.User.{$broadcastingConfig['userId']}";
}
}
?>

<!-- Notification Bell Icon -->
<div class="notifications-wrapper notifications-mode-<?= h($mode) ?>" data-theme="<?= h($theme) ?>">
<a class="notifications-bell"
id="<?= h($bellId) ?>"
Expand Down Expand Up @@ -98,16 +112,19 @@
</div>

<?php if ($mode === 'panel'): ?>
<!-- Backdrop for panel mode -->
<div class="notifications-backdrop" id="notificationsBackdrop" style="display: none;"></div>
<div class="notifications-backdrop" id="notificationsBackdrop" style="display: none;"></div>
<?php endif; ?>
</div>

<?= $this->Html->css('Crustum/NotificationUI.notifications', ['raw' => Configure::read('debug')]) ?>
<?= $this->AssetCompress->script('Crustum/NotificationUI.notifications', ['raw' => Configure::read('debug')]) ?>

<?php if ($broadcastingConfig): ?>
<?php if ($broadcasterType === 'mercure'): ?>
<?= $this->AssetCompress->script('Crustum/NotificationUI.mercure-broadcasting', ['raw' => Configure::read('debug')]) ?>
<?php else: ?>
<?= $this->AssetCompress->script('Crustum/NotificationUI.broadcasting', ['raw' => Configure::read('debug')]) ?>
<?php endif; ?>

<script>
window.broadcastingConfig = <?= json_encode($broadcastingConfig) ?>;
Expand All @@ -127,9 +144,5 @@
badgeId: <?= json_encode($badgeId) ?>,
markReadOnClick: <?= json_encode($markReadOnClick) ?>
});

console.log('Notification system initialized', {
broadcasting: <?= json_encode($broadcastingConfig !== null) ?>
});
});
</script>
16 changes: 5 additions & 11 deletions webroot/js/BroadcastingModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,14 @@ class BroadcastNotificationsModule {
if (this.echo.connector && this.echo.connector.pusher) {
this.echo.connector.pusher.connection.bind('connected', () => {
this.connected = true;
console.log('Broadcasting Module: Connected to WebSocket');
this.subscribeToChannel();
});

this.echo.connector.pusher.connection.bind('disconnected', () => {
this.connected = false;
console.log('Broadcasting Module: Disconnected from WebSocket');
});

this.echo.connector.pusher.connection.bind('error', (error) => {
console.error('Broadcasting Module: Connection error', error);
console.warn('Broadcasting Module: Connection error', error);
});
}
}
Expand All @@ -93,12 +90,9 @@ class BroadcastNotificationsModule {
return;
}

console.log(`Broadcasting Module: Subscribing to ${this.options.channelName}`);

const channel = this.echo.private(this.options.channelName);

channel.subscription.bind('pusher:subscription_succeeded', () => {
console.log(`Broadcasting Module: Subscription succeeded for ${this.options.channelName}`);
});

channel.subscription.bind('pusher:subscription_error', (error) => {
Expand All @@ -108,7 +102,6 @@ class BroadcastNotificationsModule {
this.echo.connector.pusher.bind_global((eventName, data) => {
if (eventName.startsWith('pusher:')) return;

console.log('Broadcasting Module: Event received', { event: eventName, data });
this.handleBroadcastEvent(eventName, data);
});
}
Expand Down Expand Up @@ -144,10 +137,8 @@ class BroadcastNotificationsModule {
}
if (data.actions) {
notification.actions = data.actions;
console.log('Broadcasting Module: Actions found', data.actions);
}

console.log('Broadcasting Module: Final notification', notification);
this.manager.addNotification(notification);
}

Expand All @@ -171,12 +162,15 @@ document.addEventListener('DOMContentLoaded', () => {
const broadcastConfig = window.broadcastingConfig;
if (!broadcastConfig) return;

if (broadcastConfig.broadcaster === 'mercure') {
return;
}

const initBroadcasting = () => {
try {
const manager = window.CakeNotificationManager.get();
const module = new BroadcastNotificationsModule(broadcastConfig);
module.init(manager);
console.log('Broadcasting Module: Initialized');
} catch (e) {
console.error('Broadcasting Module: Initialization failed, retrying...', e.message);
setTimeout(initBroadcasting, 100);
Expand Down
Loading