# Events

> A list of chat room events, the data each one receives, and what is actually blocked when a before handler returns false.

You receive what happens in a chat room with `U.chat(room).on(name, function (room, data) { ... })`. This page lists, based on the code, when each event fires, what `data` contains, and what is actually blocked when a `before.` handler returns `false`. For how to register and remove handlers, see [U.chat() and the room object](/en/doc/3.2#on-off).

## Rules {#rules}

- Most events come in `before.name` and `after.name` pairs. `before.` fires before the default handling, `after.` fires after it.
- Callbacks receive `(room, data)`. `room` is the [room object](/en/doc/3.2).
- If a `before.` callback returns exactly `false`, the callbacks after it are not called, and only the handling listed in the "If `before` returns false" column below stops. **What gets blocked differs per event.**
- If you change values in `data` inside `before.`, the handling that follows (display, sending) uses the changed values. The table notes which events you can modify.
- When the default handling is blocked, `after.` does not fire either.

## Event list {#events}

| Event | When | `data` | If `before` returns false |
| --- | --- | --- | --- |
| `before.create` | Room installation starts (before settings and skin are loaded). Fires again on every reinstall | `{}` | Cannot be blocked (installation continues) |
| `after.create` | Room screen is ready (before connecting). Fires again on every reinstall (`room.reload()`) | `{ id }` | — |
| `before.join` | Right before the join request is sent to the server. Fires again on every reconnect | `{ user }` | Rooms without member integration: the join request is not sent. Rooms with member integration: see the warning below |
| `after.join` | Join completed and your info received. Fires again on every reconnect | `{ my }` | (no `before`) |
| `before.message`, `after.message` | One chat message line (including past messages and your own messages) | [Message](#message) | Not shown on screen |
| `before.whisper`, `after.whisper` | A whisper (including the echo of whispers you sent) | [Whisper](#whisper) | Not shown on screen |
| `before.send`, `after.send` | When you send a message (excluding `/command` and `!command` input) | `{ message, bold, italic, underline, color }` | Not sent. `room.action.send()` returns `false` |
| `before.command`, `after.command` | When a command runs | `{ type, data }` | Not run. `room.action.command()` returns `false` |
| `before.plugin`, `after.plugin` | When channel data arrives, or when you enter a `!command` | [Channel](#plugin) | Received data: `onReceived` callbacks are not called. `!command`: `addParser` callbacks are not called and `room.action.plugin()` returns `false` |
| `before.user.join`, `after.user.join` | Someone else joins. For cases where it does not fire, see [User changes](#users) | `{ user }` | The join notice is not shown. The user is already in `room.user.list` |
| `before.user.quit`, `after.user.quit` | Someone else leaves | `{ user: { nick } }` | The leave notice is not shown and the user is not removed from `room.user.list` |
| `before.user.modi`, `after.user.modi` | Someone's (including your own) nickname, role, or mute changes | `{ target, changed }` | Not applied to `room.my`, `room.user.list`, or the screen. `room.user.get()` may already return the new value. We recommend not blocking this |
| `before.user.count`, `after.user.count` | Only the user count changes (for when this fires instead of join/leave, see [User changes](#users)) | `{ count }` | The user count on screen is not updated |
| `before.update`, `after.update` | Room settings change | Changed values `{ key: value }` | Not applied to `room.info` or the screen |
| `before.system`, `after.system` | System notice lines (notices, bans, mutes, calls, etc., including `room.print`) | `{ type, data }` | Not shown on screen |
| `before.control`, `after.control` | Server control signals (clearing the chat, vote status, etc.) | `{ type, data }` | Not passed to the skin |
| `before.error`, `after.error` | Errors (server errors and client-side errors such as command argument checks) | `{ code }` | The error is not shown on screen |
| `before.disconnect`, `after.disconnect` | The connection drops (does not fire when leaving the page or removing the tag) | `{}` | The skin is not notified, and automatic reconnection after a network error does not happen |

Blocking in `before.` does not undo what happened on the server. For example, hiding a ban notice with `before.system` does not stop the ban, and blocking `before.user.quit` does not keep that person in the room. In most cases, only **what appears on your own screen** changes.

## Order of events {#order}

**When the room opens**

1. `before.create` — installation starts. `room.info` and `room.my` are still empty.
2. Room settings, language, plugins, and skin are loaded (plugin scripts run here).
3. `after.create` — the skin screen is ready. Register skin UI and channels here.
4. `before.join` → join request to the server → `after.join`.
5. Past messages and notices arrive as `message`, `whisper`, and `system` events.

In rooms that show a join check (bot check) screen, step 1 starts after the check passes. If you reconnect because of a network problem, events start again from step 4.

**When you send a message**

1. `before.send` → sent to the server → `after.send`
2. The server sends the message to everyone in the room (including you).
3. `before.message` → shown on screen → `after.message`

`after.send` only means "sent to the server", not that it was accepted. If the server rejects it, the message does not come back; instead you get an `error` event (no permission 410, repeated content 430, etc.), a `system` event (`muted`, `bad_word`), or a `control` event (`slowWait`). Use `after.message` to confirm that the message actually went through.

If the chat input starts with `/`, it goes to the `command` event instead of `send`; if it starts with `!`, it goes to the `plugin` event. The default skin sends a `!command` as a normal message when no channel handles it or it is blocked.

**When the connection drops**

1. `before.disconnect` → skin notified → automatic reconnect decided → `after.disconnect`
2. If the drop was caused by a network error, the client reconnects and events start again from `before.join`.

If you were banned, or the connection dropped right after the server sent an error, the client does not reconnect. The `disconnect` event **does not fire** when the browser is closed, the user navigates to another page, the `<u-chat>` tag is removed, or `room.reload()` is called. Do anything that must happen before leaving in the browser's `pagehide` event.

## Join {#join}

```js
U.chat('my-room').on('after.join', function (room, data) {
	// data.my is a user object with the same shape as room.my
	room.print('Welcome, ' + data.my.nick + '.');
});
```

`before.create` fires when there is no screen and no info yet, so you rarely need it. It cannot be blocked; to change join info, use `before.join`.

`data.user` in `before.join`:

| Room | `data.user` | What you can change |
| --- | --- | --- |
| Room without member integration | `{ nick }` — the nickname saved in this browser (empty if none) | Changing `data.user.nick` requests the join with that nickname |
| Room with member integration | The decoded `user_data` (or `user_data_url`): the keys you put in `user_data`, plus `time` and `hash` | Do not change it. The signature will not match and you get error 202 |

> **Warning:** In a room with member integration, returning `false` from `before.join` does not block the join. Instead, the request is sent with empty join info and ends in an error. Do not use `before.join` to block joining in rooms with member integration. To keep someone out, leave out `user_data` or do not add the tag.

## Message {#message}

```js
U.chat('*').on('after.message', function (room, data) {
	console.log(data.user.nick, data.content);
});
```

| Key | Description |
| --- | --- |
| `user` | The sender's [user object](/en/doc/3.2#user-object) |
| `content` | Content |
| `style` | `{ bold, italic, underline, size, color }`. If `color` is missing, the room's default text color applies |
| `created` | Time sent, `'YYYY-MM-DD HH:MM:SS'` (in the viewer's time zone) |
| `time` | Time sent (Unix seconds) |
| `uid` | Unique message ID (hex string). Empty if none |
| `past` | `true` for past chat received again when joining or reconnecting. Absent on newly posted messages |

- If you change `data.content` in `before.message`, the changed content is shown **only on your screen**.
- Messages from people you ignore (below moderator) reach `before.message` only; they are not shown and `after.message` does not fire.

## Whisper {#whisper}

| Key | Description |
| --- | --- |
| `from`, `to` | The sender's and recipient's [user objects](/en/doc/3.2#user-object) |
| `content` | Content |
| `style` | Currently always an empty string. Its shape differs from the message `style` object |
| `created`, `time` | Time sent (string, Unix seconds) |

- The server sends a whisper to both the sender and the recipient, so whispers you send also come back through this event. If `data.from.nick` is your nickname, you sent it.
- Unlike messages, there is no `uid`.
- When you join, whispers involving you arrive again along with past messages.
- If the whisper is from someone you ignore (below moderator), or your settings refuse whispers, it reaches `before.whisper` only; it is not shown and `after.whisper` does not fire.

## Sending and commands {#send-command}

```js
// Append a signature to outgoing messages
U.chat('*').on('before.send#sign', function (room, data) {
	data.message += ' ~';
});

// Block the whisper command
U.chat('*').on('before.command#no-whisper', function (room, data) {
	if (data.type === 'whisper') return false;
});
```

- If you change `message`, `bold`, `italic`, `underline`, or `color` in `before.send`, the changed values are sent.
- The `type` in `before.command` is the name in lowercase with `_` removed (`un_ignore` → `unignore`). If you change `data.data` (the argument array), the command runs with the changed arguments.

## Channel {#plugin}

`before.plugin`/`after.plugin` fire in two cases, with a different `data` shape for each.

| Case | `data` |
| --- | --- |
| Channel data sent by someone else or the server arrives | `{ user, type, data }` — `user` is the sender, `type` is `0` (to everyone) or `1` (to me only), `data` is the value array |
| `!name args` entered in the chat input (or `room.action.plugin()`) | `{ type: 'name', data: ['arg', ...] }` — `type` is the channel name |

Data sent by the server (REST API) and channel saved values received again on join have `user.auth` set to `4` and an empty nickname. Check whether data came from the server only with `data.user.auth === 4`.

Received data does not include the channel name. To receive data per channel, use `room.plugin.add(channel).onReceived`. See [Channel API and limits](/en/doc/3.5).

## User changes {#users}

```js
U.chat('*').on('after.user.modi', function (room, data) {
	// data.target  nickname before the change
	// data.changed whichever of { nick?, auth?, mute? } changed
	if (data.changed.nick) room.print(data.target + ' → ' + data.changed.nick);
});
```

- Fires only for changes after joining. It also fires when your own nickname, role, or mute changes, and `room.my` changes too. For the role at join time, look at `data.user` in `user.join` or `data.my` in `after.join`.
- Inside `before.user.modi`, `room.my` and `room.user.list` have not changed yet. `room.user.get()` may already return the new value.
- In some cases `user.join` and `user.quit` do not fire, and only `user.count` (every 3 seconds) does:
  - **The default skin.** This is always the case, whether the user list is open or closed.
  - The legacy skin while the user list is closed
  - Rooms with 100 or more users at the time you join
- In these cases, joins and leaves are reflected only in the user list on screen; there is no event telling the page script who joined or left. `room.user.list` is not updated either. The only thing the page script can rely on is the user count (`user.count`).
- Build features such as join/leave notifications on the assumption that they only work in small rooms using the legacy skin with the user list open. If your server needs to know, use the user count from the [Server REST API](/en/doc/3.6#stats).
- The `user.list` event (`{ list }`) fires only in legacy skins. The default skin receives the user list in parts, so this event does not fire.

## System notices {#system}

`data.type` is the notice type and `data.data` is the value array. The shape of the value array differs by type.

| `type` | Meaning |
| --- | --- |
| `notice` | Notice, `room.print()` |
| `ban` | Ban |
| `mute`, `permanent_mute` | Mute (timed, permanent) |
| `un_mute` | Mute lifted |
| `to_call`, `from_call` | Call sent, received |
| `invite`, `invite_notice` | Private channel invite received, sent |
| `ignore`, `un_ignore` | Ignore, unignore |
| `frozen`, `unfrozen` | Room frozen, unfrozen |
| `op`, `deop` | Moderator assigned, removed |
| `user_info` | Result of the `info` command |
| `ip` | Result of the `ip` command |
| `admin_login`, `admin_logout` | Someone became admin with `/login`, stepped down |
| `clear_log` | Chat cleared |
| `muted` | Your message was rejected because you are muted |
| `bad_word` | Your message was rejected by the word filter |
| `retry_connect` | Reconnect attempt (number of attempts) |

```js
U.chat('*').on('after.system', function (room, data) {
	if (data.type === 'from_call') window.focus();
});
```

For users who have turned off receiving calls or invites, even `before.system` for `from_call` and `invite` does not fire.

## Control signals {#control}

`data.type` is the signal name and `data.data` is the value array. Most of these are internal signals used by the skin, so names and shapes may change. The ones useful in page scripts are roughly these:

| `type` | Meaning | `data` |
| --- | --- | --- |
| `clearLog` | Chat cleared | `[]` |
| `removeChat` | An admin deleted messages | List of `uid`s of deleted messages. Match them against the `uid` of `message` |
| `slowWait` | Your message was rejected because of slow mode | `[remaining milliseconds, content you tried to send]` |
| `pluginDataReplaced` | A channel saved value was replaced in a free room | `[]`. See [Channel API and limits](/en/doc/3.5#save) |

## Errors {#error}

`data.code` is the error number, and the message in the current language is in `room.language.error[code]`.

```js
U.chat('*').on('after.error', function (room, data) {
	console.warn(data.code, room.language.error[data.code]);
});
```

| Code | Meaning |
| --- | --- |
| `101` | Protocol error (invalid value, channel saved value too large, etc.) |
| `201` | Room does not exist |
| `301` | Nickname already in use |
| `202` | Member integration signature does not match |
| `203` | Connected to a member integration room without login info |
| `205` | Login info older than the time sync limit, or the same `user_data` more than 5 minutes after its first use |
| `207` | Room is full |
| `208`, `701` | Wrong password, password required |
| `402` | No permission |
| `404` | Target not found |
| `407` | Command cannot be used on yourself |
| `410` | No chat permission (feature condition) |
| `412` | Invalid command arguments |
| `415` | You are banned |
| `416` | Command not available in this room |
| `421` | Already has that role |
| `422` | Wrong password |
| `413` | You are muted |
| `418` | Room is frozen |
| `424` | Cannot send because you are refusing whispers |
| `425` | Sending too often |
| `428` | Channel sending limit exceeded |
| `429` | Too many login attempts |
| `430` | Sending the same content repeatedly |
| `501`, `502` | No private channel; cannot create a private channel inside a private channel |

For causes and fixes of member integration errors, see [Member integration](/en/doc/2.1#troubleshooting).

## data examples {#data-examples}

These are the actual shapes of `data` for each event. Values are examples, and keys the server did not send may be `undefined`. To check for yourself, log it to the console, for example `U.chat('*').on('after.message', function (room, data) { console.log(data); })`.

```js
// after.join
{ my: { nick: '홍길동', id: 'user1', level: 5, auth: 1, icons: '', nickcon: '',
        connected: '2026-10-17 12:00:00', time: 1792209600, mute: 0, session: 'a1b2c3d4e5f6', profileimg: '' } }

// before.join (room without member integration)
{ user: { nick: '홍길동' } }

// after.message
{ user: { nick: '홍길동', auth: 1, ... },
  content: 'Hello',
  style: { bold: false, italic: false, underline: false, size: '9pt', color: '#000000' },
  created: '2026-10-17 12:00:01', time: 1792209601, uid: '0a1b2c3d4e' }

// after.whisper
{ from: { nick: '홍길동', ... }, to: { nick: '김철수', ... }, style: '', content: 'One moment',
  created: '2026-10-17 12:00:02', time: 1792209602 }

// before.send
{ message: 'Hello', bold: undefined, italic: undefined, underline: undefined, color: undefined }

// before.command  (/w 김철수 One moment)
{ type: 'whisper', data: ['김철수', 'One moment'] }

// after.user.join / after.user.quit
{ user: { nick: '김철수', auth: 0, ... } }
{ user: { nick: '김철수' } }

// after.user.modi
{ target: '김철수', changed: { nick: '철수' } }
{ target: '김철수', changed: { auth: 2 } }
{ target: '김철수', changed: { mute: 1 } }

// after.user.count
{ count: 152 }

// after.update
{ slowSeconds: 5 }

// after.system
{ type: 'mute', data: ['Admin', '김철수', 10, 'Spamming'] } // admin, target, minutes, reason
{ type: 'from_call', data: ['김철수', '홍길동'] }       // sender, recipient

// after.control
{ type: 'removeChat', data: ['0a1b2c3d4e'] }

// after.plugin  (channel data received / !dice 1 6 entered)
{ user: { nick: '김철수', auth: 1, ... }, type: 0, data: [4] }
{ type: 'dice', data: ['1', '6'] }

// after.error
{ code: 402 }
```
