Skip to main content
Markdown

Events from the page, in Laravel

Pages send events to the rooms they are in, straight through WordPlus Cloud, with no request to your app: chat messages, typing, read ticks, a cursor. The options of a room say which events its pages may send.

The chat window of Ben shows “Ann typing” while Ann, in her own browser, writes a reply

What a room allows​

// routes/channels.php: the chat of Ann and Ben. They send messages, typing, reactions and read ticks. The messages and
// reactions reach this app by webhook, which keeps the conversation.
Broadcast::channel( 'chat.{a}.{b}', fn ( User $user, string $a, string $b ) => in_array( (string) $user->id, [ $a, $b ], true ),
[ 'history' => 50, 'emit' => [ 'message', 'typing', 'reaction', 'read' ], 'webhook' => [ 'message', 'reaction' ] ] );
  • emit lists the names, or '*' for any. None by default: a room your app alone sends to.
  • webhook names those your app gets too, to keep them (below).
  • history keeps the last events of the room for 30 minutes: what a page that reconnects missed comes again from it.
  • The answer for one user can narrow it, from a room of the facade: [ 'emit' => [ 'typing' ] ], or [ 'readOnly' => true ].
  • Reserved: joined, join, leave, count and error are the names kept for the room itself.

Sending​

A page sends to a room it has joined, and hears what the others send:

const chat = rt.join( 'chat.34.42' );

chat.emit( 'message', { text: 'Hi' } ); // sent, nothing waited for
const { id, at } = await chat.emitWithAck( 'message', { text: 'Hi' } ); // WordPlus Cloud took it: checked, kept, sent
chat.volatile.emit( 'typing', { on: true } ); // dropped rather than queued while off line, never kept
await chat.timeout( 5000 ).emitWithAck( 'message', { text: 'Hi' } ); // an ack waited for 5 seconds at most

chat.on( 'message', ( data, meta ) => draw( data.text, meta.from ) ); // meta: { from, id, at }
  • Everyone in the room gets it, the other tabs and devices of the sender too. The tab that sent it does not, so it draws its own at once.
  • emitWithAck resolves with the kept event id and time, or rejects with a code: not_allowed (the room does not list it), forbidden, rate_limited, too_large.
  • meta.from is the sender user id, as your app vouched for them: a page cannot send as someone else.
  • Binary: an ArrayBuffer, Uint8Array or Blob may be anywhere in the data. It is sealed as binary.
  • Limits: 1 MB an event, 50 a second a connection, in bursts of up to 100.

Typing, done the usual way​

let sent = 0;
input.addEventListener( 'input', () => {
if ( Date.now() - sent < 2000 ) return; // at most every 2 seconds
sent = Date.now();
chat.volatile.emit( 'typing', { on: true } );
} );

chat.on( 'typing', ( data, meta ) => showTyping( meta.from, data.on ) ); // and hide it after 4 seconds of nothing

Keeping the conversation​

The conversation is for your app to keep, in its own database, as Better Messages keeps its own in the WordPress database. A room keeps its events for 30 minutes, for pages that reconnect. Your app gets the events the room names in webhook (webhooks) and saves them by event_id, the id the pages saw as meta.id. A route gives them back, a page at a time:

// the boot() of a service provider
$this->callAfterResolving( Realtime::class, function ( Realtime $realtime ) {
$realtime->on( 'message', fn ( array $e ) => Message::firstOrCreate( [ 'event_id' => $e['event_id'] ], [
'room' => $e['room'], 'from_id' => $e['from'], 'text' => $e['data']['text'], 'created_at' => Carbon::createFromTimestampMs( $e['at'] ),
] ) );
} );

// routes/web.php: the newest 50, or with ?before=<id> the 50 before that one, oldest first. Only its two people may
// read it.
Route::get( '/chat/{a}/{b}/messages', function ( Request $request, string $a, string $b ) {
abort_unless( in_array( (string) $request->user()->id, [ $a, $b ], true ), 403 );
$before = Message::firstWhere( 'event_id', $request->query( 'before' ) );
$page = Message::where( 'room', "chat.{$a}.{$b}" )->when( $before, fn ( $q ) => $q->where( 'id', '<', $before->id ) )
->orderByDesc( 'id' )->limit( 51 )->get();

return [ 'messages' => $page->take( 50 )->reverse()->values()->map->toChat(), 'more' => $page->count() > 50 ]; // [ id, from, text, at, reactions ]
} )->middleware( 'auth' );
  • Check there anything that matters: the events do not go through your app before they are sent on.
  • at is in milliseconds, and reactions maps each reaction to who chose it. A reaction comes as { id, key }: the one reaction of the user to message id, an empty key taking it back.

Ready-made: the chat​

The Better Messages conversation, on any room: your app conversation paged back as the reader scrolls up, the room filling in what came since, the day separators, the messages of a sender stacked, typing, reactions, the read ticks and the composer. It is one element:

<wordplus-chat room="chat.{{ $chat->a }}.{{ $chat->b }}" messages="/chat/{{ $chat->a }}/{{ $chat->b }}/messages" placeholder="{{ __( 'Write a message' ) }}"></wordplus-chat>
await rt.callScreen();   // loads the elements and the call screen, rt.ui() the elements without calls
  • messages is your route, asked with the page cookies. chat.load = async ( before ) => ( { messages, more } ) replaces it, for a token or another shape: before is null for the newest page, or the oldest message shown ({ id, at }). With neither, it shows what the room still has.
  • Its room lets it send what it uses: 'emit' => [ 'message', 'typing', 'reaction', 'read' ], and names message and reaction in 'webhook' for your app to keep.
  • What it sends is what you would: message as { text } with an ack, typing as { on } and read as { id }, both volatile, and reaction as { id, key }. Your own pages and apps on the same room read it.
  • Names and pictures come from your directory.
  • It fills the height its place gives it. Its look is that of Better Messages, in a shadow root. Its colours are CSS custom properties your page sets.

<wordplus-typing room="…"> is the typing line alone, for a page that draws its own messages.

More, with Node and Python side by side: events in depth.