Events
How to send events to users?
Let's look at the example of a new alert notification.
Create a listener for the entity_post_save
event with the hint XF\Entity\UserAlert
.
Then create a new broadcasting event with UpdateAlerts
name in YourAddon/Broadcasting/Events
folder:
namespace YourAddon\Broadcasting\Events;
use BS\XFWebSockets\Broadcasting\Event;
use BS\XFWebSockets\Broadcasting\UserChannel;
use XF\Entity\UserAlert;
class UpdateAlerts implements Event
{
public UserAlert $fromAlert;
/**
* Event constructor.
* Save the alert from which we should load new alerts.
*
* @param \XF\Entity\UserAlert $fromAlert
*/
public function __construct(UserAlert $fromAlert)
{
$this->fromAlert = $fromAlert;
}
/**
* The channels to which the event will be broadcast.
*/
public function toChannels(): array
{
return [
// Broadcast to the alert recipient's private channel.
new UserChannel($this->fromAlert->alerted_user_id)
];
}
/**
* The data to be broadcast.
*/
public function payload(): array
{
return [
'from_alert_id' => $this->fromAlert->alert_id
];
}
/**
* Event name.
* Optional method.
* By default, the name of an event is its class.
*/
public function broadcastAs(): string
{
return 'UpdateAlerts';
}
}
Now we can broadcast this notification in the listener we created:
use BS\XFWebSockets\Broadcast;
use XF\Entity\UserAlert;
public static function postSaveUserAlert(UserAlert $alert)
{
if (! $alert->isInsert()) {
return;
}
Broadcast::event(
'YourAddon:UpdateAlerts',
$alert
);
// or use the event class
Broadcast::event(
\YourAddon\Broadcasting\Events\UpdateAlerts::class,
$alert
);
// or use the event object
Broadcast::event(
new \YourAddon\Broadcasting\Events\UpdateAlerts($alert)
);
}
Users will now receive an UpdateAlerts
event with the from_alert_id
parameter on every alert.
How to handle events?
Let's look at an example of handling the UpdateAlerts
event we just created.
Create a new template modification to import watch.js
file:
- Template:
helper_js_global
- Modification key:
your_adddon_helper_js_global_watch_js
- Search type: Simple replacement
- Find:
<!--XF:JS-->
- Replace:
$0
<xf:if is="$xf.visitor.user_id">
<xf:js src="your_addon/watch.js" min="1" />
</xf:if>
Create a watch.js
in the js/your_addon
folder with the following content:
(( window, document ) => {
"use strict";
XF.RealTimeAlerts ??= {
init () {
/*
* Listen to the `UpdateAlerts` event on the `visitor` channel.
* The `visitor` channel is created automatically when the user is logged in.
*
* getWebsocketsPromise() is a function that returns a promise
* that resolves when the WebSockets connection is established and you can subscribe to channels.
* It resolves { manager: window.ws.manager, echo: window.ws.echo }.
*
* ?. is the optional chaining operator, help to avoid errors when user has no permission to use WebSockets.
*/
window.getWebsocketsPromise?.().then(( { echo, manager } ) => {
manager.channels['visitor']
.listen('UpdateAlerts', XF.proxy(this, 'updateAlerts'))
})
},
updateAlerts ( { from_alert_id } ) {
// Send a request to the server to get new alerts.
}
}
ws.appStateManager?.ensureReady()?.then(() => XF.RealTimeAlerts.init())
})(window, document)
Channels
Channels are a way to send messages to a group of users. For example, you can send a message to all users in a conversation or to all users in a forum. Or you can send a message to a specific user.
There are three types of channels:
- Public: access is not limited in any way, all users can connect.
- Presence: access can be limited. Makes possible to get a list of users present in the channel.
- Private: access to the channel requires a signature, which is issued if the user has permission to access the channel.
How to add a channel?
Create your own channel class in the YourAddon/Broadcasting
directory. It must extends one of the base classes:
BS\XFWebSockets\Broadcasting\Channel
- for public channels.BS\XFWebSockets\Broadcasting\PrivateChannel
- for private channels.BS\XFWebSockets\Broadcasting\PresenceChannel
- for presence channels.
Create a new listener for the broadcast_channels
event:
use YourAddons\Broadcasting\ThreadChannel;
public static function broadcastChannels(array &$channels)
{
$channels[ThreadChannel::NAME_PATTERN] = ThreadChannel::class;
}
How to authorize user access to a channel?
Add a join
method to your channel that returns a boolean:
namespace YourAddon\Broadcasting;
use BS\XFWebSockets\Broadcasting\PresenceChannel;
use XF\Entity\User;
class ThreadChannel extends PresenceChannel
{
public const NAME_PATTERN = 'Thread.{id}';
public function join(User $visitor, int $id): bool
{
$thread = \XF::em()->find('XF:Thread', $id);
if (!$thread) {
return false;
}
return \XF::asVisitor($visitor, fn() => $thread->canView());
}
}