Skip to content

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:

php
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:

php
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:
html
$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:

js
(( 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:

php
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:

php
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());
    }
}