---
sidebar_position: 5
sidebar_label: Events from the app
---

# Events from the app

The app sends events to the rooms it is in, straight through WordPlus Cloud, with no request to your server: chat messages, typing, read ticks, reactions. A room rule says which events may be sent to it ([events in depth](../apps/events.md)):

**Node**

```ts
realtime.room( 'chat:{a}:{b}', { history: 50, emit: [ 'message', 'typing', 'reaction', 'read' ], webhook: [ 'message', 'reaction' ] },
    ( user, { a, b } ) => [ a, b ].includes( String( user ) ) );
```

**PHP**

```php
$realtime->room( 'chat:{a}:{b}', fn ( $user, array $p ) => in_array( (string) $user, [ $p['a'], $p['b'] ], true ),
    [ 'history' => 50, 'emit' => [ 'message', 'typing', 'reaction', 'read' ], 'webhook' => [ 'message', 'reaction' ] ] );
```

**Python**

```python
realtime.room("chat:{a}:{b}", lambda user, p: str(user) in (p["a"], p["b"]),
              history=50, emit=["message", "typing", "reaction", "read"], webhook=["message", "reaction"])
```

## Sending

```kotlin
val chat = rt.join("chat:34:42")

chat.emit("message", buildJsonObject { put("text", "Hi") })                         // sent, nothing waited for
val ack = chat.emitWithAck("message", buildJsonObject { put("text", "Hi") })        // WordPlus Cloud took it: ack.id, ack.at
chat.volatile.emit("typing", buildJsonObject { put("on", true) })                   // dropped rather than queued while off line, never kept
chat.emitWithAck("message", buildJsonObject { put("text", "Hi") }, timeoutMs = 5_000)

@Serializable data class Message(val text: String)
chat.emitWithAck("message", Json.encodeToJsonElement(Message("Hi")))              // your own types, through kotlinx.serialization
```

- **Everyone in the room** gets it, the other pages and devices of the sender too. This app does not.
- **`emitWithAck`** suspends until WordPlus Cloud has checked the event, kept it (when the room keeps history) and sent it on, and returns the kept event `id` and time. It throws a `RealtimeException` with its `code`: `not_allowed` (the rule does not list it), `forbidden` (not in the room), `rate_limited`, `too_large`, `bad_event`, `timeout`, or `offline` while the connection is down.
- **Before the room is joined,** an emit waits for it.
- **`meta.from`** on the other side is this app user, as your server vouched for them: an app cannot send as someone else.
- **Binary:** `bytes.toJson()` puts a `ByteArray` in the data, and `element.bytes()` reads one back.
- **Limits:** 1 MB an event (kept in history up to 64 KB), 50 a second a connection, in bursts of up to 100.

## Hearing them

```kotlin
@Serializable data class ChatMessage(val text: String = "")
@Serializable data class Typing(val on: Boolean = false)

viewModelScope.launch { chat.eventsOf<ChatMessage>("message").collect { (message, meta) -> add(meta.from, message.text, meta.id) } }
viewModelScope.launch { chat.eventsOf<Typing>("typing").collect { (typing, _) -> showTyping(typing.on) } }
```

A message the app sent with `emitWithAck` does not come back to it: add it from the ack, with `ack.id`, so it is not shown twice when your server page or the room history has it too.

## Keeping the conversation

The conversation is for your app to keep, on your server, as Better Messages keeps its own in the WordPress database. A room keeps its events for 30 minutes, for apps that reconnect. Your server gets the events the rule names in `webhook` and saves them by `event_id`, the id the app saw as `meta.id`, and a route of yours gives them back a page at a time, with the user token ([keeping the conversation](../apps/events.md#keeping-the-conversation)):

```kotlin
// What was said before: the conversation your server keeps, then what the room has that it may not have yet.
chat.on("joined") { _, _ ->
    viewModelScope.launch {
        val page = withContext(Dispatchers.IO) {
            val request = Request.Builder().url("$server/chat/34/42/messages").header("Authorization", "Bearer $token").build()   // ?before=<id> for the page before
            http.newCall(request).execute().use { Json.parseToJsonElement(it.body!!.string()).jsonObject }   // { messages: [ { id, from, text, at, reactions } ], more }
        }
        for (m in page["messages"]!!.jsonArray) add(m.jsonObject["from"]!!.jsonPrimitive.content, m.jsonObject["text"]!!.jsonPrimitive.content, m.jsonObject["id"]!!.jsonPrimitive.content)
        for (e in chat.history(limit = 50).events) if (e.event == "message") add(e.meta.from, e.data.jsonObject["text"]!!.jsonPrimitive.content, e.meta.id)
    }
}
```

- **One id for each message,** whichever way it came, so `add` keeps the first.
- **The events do not go through your server** before they are sent on, so check there anything that matters.

## Talking with the web chat

The Better Messages chat on your web pages (`<wordplus-chat>`) sends these events, so an app that sends the same reads with it, both ways:

| Event | Data | |
|---|---|---|
| `message` | `{ text }` | with an ack. Your server keeps it |
| `typing` | `{ on }` | volatile |
| `read` | `{ id }`, the newest message seen | volatile, for the sender ticks |
| `reaction` | `{ id, key }`, `key` empty to take it back | with an ack. Your server keeps it |
