# Better Messages - Hooks & Functions Reference
> PHP and JavaScript hooks, filters, and functions for extending and customizing Better Messages plugin.
This file contains all documentation content in a single document following the llmstxt.org standard.
## Better Messages JavaScript Actions Reference — Client-Side Hooks
# JavaScript Actions Reference
:::info REQUIREMENTS
JavaScript hooks use the WordPress Hooks API (`wp.hooks`). Add your code to a custom JavaScript file enqueued after Better Messages.
:::
This page lists all JavaScript action hooks available in the Better Messages plugin frontend. Actions allow you to execute custom code when specific events occur on the client side.
---
## Messages
### `better_messages_message_received` {#better_messages_message_received}
Fires when a new message is received — via WebSocket, AJAX polling, or BroadcastChannel relay between tabs.
**Parameters:**
- `message_id` *(number)* — The received message ID
```js
wp.hooks.addAction('better_messages_message_received', 'my_plugin', function(messageId) {
console.log('New message received: #' + messageId);
// Update external badge, trigger sound, etc.
});
```
---
### `better_messages_on_local_notification` {#better_messages_on_local_notification}
Fires immediately before showing a browser notification for an incoming message. Only fires when the message sender is not the current user, the thread is not muted, and no active tab is present.
**Parameters:**
- `message` *(object)* — Full message object (message_id, thread_id, sender_id, message content, etc.)
```js
wp.hooks.addAction('better_messages_on_local_notification', 'my_plugin', function(message) {
// Send to custom notification system
myNotificationService.notify({
title: 'New message',
body: message.message,
threadId: message.thread_id
});
});
```
---
### `better_messages_realtime_event` {#better_messages_realtime_event}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Receives custom real-time events sent via the WebSocket server. To send events that trigger this hook, use the [`send_realtime_event`](/hooks/php-functions#send_realtime_event) PHP function.
**Parameters:**
- `eventData` *(object)* — Decrypted custom event data (structure defined by sender)
```js
wp.hooks.addAction('better_messages_realtime_event', 'my_plugin', function(eventData) {
console.log('Custom event received:', eventData);
if (eventData.type === 'order_update') {
showOrderNotification(eventData.order_id);
}
});
```
---
## Unread Counts
### `better_messages_update_unread` {#better_messages_update_unread}
Fires when the total unread message count changes across all threads. Also re-fires on window load for late-loading scripts.
Since 3.2.0 the total includes the unread messages that plugins add with the [`better_messages_total_unread`](/hooks/js-filters#better_messages_total_unread) filter, and the action fires again when one of them runs [`better_messages_refresh_total_unread`](#better_messages_refresh_total_unread).
**Parameters:**
- `unread` *(number)* — Total unread count
```js
wp.hooks.addAction('better_messages_update_unread', 'my_plugin', function(unread) {
// Update custom badge element
document.querySelector('.my-badge').textContent = unread > 0 ? unread : '';
document.querySelector('.my-badge').style.display = unread > 0 ? 'block' : 'none';
});
```
---
### `better_messages_update_thread_unread` {#better_messages_update_thread_unread}
Fires when the unread count for a specific thread changes. Also re-fires on window load.
**Parameters:**
- `threadId` *(number)* — Thread ID
- `unread` *(number)* — Unread count for this thread
```js
wp.hooks.addAction('better_messages_update_thread_unread', 'my_plugin', function(threadId, unread) {
// Update per-thread badge
var badge = document.querySelector('[data-thread="' + threadId + '"] .badge');
if (badge) badge.textContent = unread;
});
```
---
### `better_messages_refresh_total_unread` {#better_messages_refresh_total_unread}
An action you fire rather than listen to: it makes the page run [`better_messages_total_unread`](/hooks/js-filters#better_messages_total_unread) again and show the new total in the page title, the counters, the mini widgets bubble and [`better_messages_update_unread`](#better_messages_update_unread). Fire it when the unread count your filter adds has changed. Adding or removing the filter itself needs no refresh, the page notices it. See [Add your unread messages to the total](/docs/development/guides/custom-tabs/#add-your-unread-messages-to-the-total). Since 3.2.0.
**Parameters:** none
```js
wp.hooks.doAction('better_messages_refresh_total_unread');
```
---
## Online Status
### `better_messages_user_online` {#better_messages_user_online}
Fires when a user transitions to online status in the presence system. The user ID has been added to the online users map with a timestamp. Use this for custom online indicators, activity logging, or triggering UI updates.
**Parameters:**
- `userId` *(number)* — User ID who came online
```js
wp.hooks.addAction('better_messages_user_online', 'my_plugin', function(userId) {
console.log('User #' + userId + ' is now online');
});
```
---
### `better_messages_user_offline` {#better_messages_user_offline}
Fires when a user transitions to offline status in the presence system. The user ID has been removed from the online users map. Use this for updating status indicators, logging activity, or cleanup operations.
**Parameters:**
- `userId` *(number)* — User ID who went offline
```js
wp.hooks.addAction('better_messages_user_offline', 'my_plugin', function(userId) {
console.log('User #' + userId + ' is now offline');
});
```
---
### `better_messages_online_users_loaded` {#better_messages_online_users_loaded}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when the initial list of online users is received from the server, and every ~30 seconds on refresh.
**Parameters:**
- `online` *(number[])* — Array of online user IDs
```js
wp.hooks.addAction('better_messages_online_users_loaded', 'my_plugin', function(onlineUsers) {
console.log('Online users:', onlineUsers.length);
// Initialize presence indicators
});
```
---
## Group Calls
### `better_messages_group_call_status` {#better_messages_group_call_status}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when the number of people in a conversation's group call changes. The call server pushes the change, so this is event-driven — you do not need to poll anything, and nothing reaches your own server. It needs the visitor to have a chat connection, which means a logged-in member or a visitor registered through guest chat.
**Parameters:**
- `threadId` *(number)* — Conversation ID
- `counts` *(object)* — `{ video, audio }`, the people in each of the two call rooms
```js
wp.hooks.addAction('better_messages_group_call_status', 'my_plugin', function(threadId, counts) {
const el = document.querySelector('#room-' + threadId + '-count');
if ( el ) {
el.textContent = counts.video;
}
});
```
Conversations the visitor takes part in are watched automatically. For any other room — a public landing page advertising a chat room, say — call [`BetterMessages.subscribeGroupCallStatus()`](/hooks/js-functions/#subscribeGroupCallStatus) first, and this action then fires for it too.
---
## Chat Rooms
### `better_messages_chat_room_join` {#better_messages_chat_room_join}
Fires after the REST API join request completes for a chat room. The `result` parameter indicates success or failure — check it before assuming the join succeeded, as permission checks may deny access.
**Parameters:**
- `result` *(object)* — API response from join request
- `chatId` *(number)* — Chat room ID
```js
wp.hooks.addAction('better_messages_chat_room_join', 'my_plugin', function(result, chatId) {
if (result) {
console.log('User joined chat room #' + chatId);
} else {
console.log('User was not allowed to join chat room #' + chatId);
}
});
```
---
### `better_messages_chat_room_leave` {#better_messages_chat_room_leave}
Fires after the REST API leave request completes for a chat room. The `result` parameter indicates success or failure — leaving may be denied if auto_join is enabled for the room.
**Parameters:**
- `result` *(object)* — API response from leave request
- `chatId` *(number)* — Chat room ID
```js
wp.hooks.addAction('better_messages_chat_room_leave', 'my_plugin', function(result, chatId) {
if (result) {
console.log('User left chat room #' + chatId);
} else {
console.log('User was not allowed to leave chat room #' + chatId);
}
});
```
---
## Conversations
### `better_messages_conversation_with_user_opened` {#better_messages_conversation_with_user_opened}
Fires after a private conversation with a user is successfully opened or created via the REST API. E2E key setup (if enabled) is complete at this point. Fires before navigation to the thread occurs, giving you a chance to intercept or augment the flow.
**Parameters:**
- `element` *(HTMLElement)* — DOM element that triggered the conversation open
- `thread_id` *(number)* — Thread ID
- `data` *(object)* — API response with thread details
- `uniqueKey` *(string|null)* — Optional unique identifier
- `useMiniChat` *(boolean)* — Whether mini chat was used
```js
wp.hooks.addAction('better_messages_conversation_with_user_opened', 'my_plugin', function(el, threadId, data, key, mini) {
console.log('Opened conversation thread #' + threadId + (mini ? ' (mini chat)' : ''));
});
```
---
## Navigation
### `better_messages_refresh_navigation_tabs` {#better_messages_refresh_navigation_tabs}
An action you fire rather than listen to: it makes the messenger's navigation bars run [`better_messages_navigation_tabs`](/hooks/js-filters#better_messages_navigation_tabs) again and draw what it returns. Fire it when something your filter reads has changed, a count for a badge say. Adding or removing the filter itself needs no refresh, the bars notice it.
**Parameters:** none
```js
wp.hooks.doAction('better_messages_refresh_navigation_tabs');
```
:::info
Available since Better Messages 3.1.5.
:::
---
### `better_messages_open_navigation_tab` {#better_messages_open_navigation_tab}
An action you fire rather than listen to: it brings a navigation tab to the front, from a button of your own, a notification, or a link that should land on your tab. Pass the tab's `key`, as [`better_messages_navigation_tabs`](/hooks/js-filters#better_messages_navigation_tabs) lists it. See [Bring your tab to the front](/docs/development/guides/custom-tabs/#bring-your-tab-to-the-front). Since 3.2.0.
**Parameters:**
- `key` *(string)* — The tab's key
```js
wp.hooks.doAction('better_messages_open_navigation_tab', 'my-leaderboard');
```
A messenger on screen selects the tab in its strip: the side panel, its collapsed rail, or the phone view. Without one, the mini widgets open their bar, or the bubble, on the tab. On a phone with neither in view, the phone view opens, on the tab when its bar has it. A tab asked for while the page is still starting opens as soon as a bar that has it is drawn, within ten seconds, unless the member picks a tab first. On a wide screen, a key that no bar on the page has does nothing.
---
## Calls
### `better_messages_call_accepted` {#better_messages_call_accepted}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when the recipient accepts an incoming call and any pending call timeouts are cleared. Use this to update call UI, start call duration timers, or log call acceptance events.
**Parameters:**
- `message_id` *(number)* — Call message ID
- `thread_id` *(number)* — Thread ID
```js
wp.hooks.addAction('better_messages_call_accepted', 'my_plugin', function(messageId, threadId) {
console.log('Call accepted in thread #' + threadId);
});
```
---
### `better_messages_call_end` {#better_messages_call_end}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when a call ends, at the beginning of the `endCall()` flow before any cleanup. The `action` parameter indicates the reason: `'abort'` (caller hung up), `'reject'`/`'rejected'` (rejected by either party), or `null` for normal call completion.
**Parameters:**
- `message_id` *(number)* — Call message ID
- `thread_id` *(number)* — Thread ID
- `action` *(string|null)* — End reason: `'abort'`, `'reject'`, `'rejected'`, or `null` (normal end)
```js
wp.hooks.addAction('better_messages_call_end', 'my_plugin', function(messageId, threadId, action) {
console.log('Call ended in thread #' + threadId + ', reason: ' + (action || 'normal'));
});
```
---
### `better_messages_call_aborted` {#better_messages_call_aborted}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when an incoming call times out with no response within the timeout period. The call video sound has been stopped and the incoming call UI has been hidden before this hook fires. Use this to clean up call UI or log missed calls.
**Parameters:**
- `message_id` *(number)* — Call message ID
- `thread_id` *(number)* — Thread ID
```js
wp.hooks.addAction('better_messages_call_aborted', 'my_plugin', function(messageId, threadId) {
console.log('Call timed out in thread #' + threadId);
});
```
---
### `better_messages_call_self_rejected` {#better_messages_call_self_rejected}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when the caller cancels their own outgoing call before the recipient responds.
**Parameters:**
- `message_id` *(number)* — Call message ID
- `thread_id` *(number)* — Thread ID
---
### `better_messages_call_user_rejected` {#better_messages_call_user_rejected}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Fires when the recipient explicitly rejects a call or is busy in another call. The `reason` parameter contains a human-readable message distinguishing between active rejection and busy status. Use this to display rejection notifications or update call history.
**Parameters:**
- `message_id` *(number)* — Call message ID
- `thread_id` *(number)* — Thread ID
- `user_id` *(number)* — User who rejected
- `reason` *(string)* — Rejection reason message
```js
wp.hooks.addAction('better_messages_call_user_rejected', 'my_plugin', function(messageId, threadId, userId, reason) {
console.log('Call rejected by user #' + userId + ': ' + reason);
});
```
---
## Better Messages JavaScript Filters Reference — Client-Side Hooks
# JavaScript Filters Reference
:::info REQUIREMENTS
JavaScript hooks use the WordPress Hooks API (`wp.hooks`). Add your code to a custom JavaScript file enqueued after Better Messages.
:::
This page lists all JavaScript filter hooks available in the Better Messages plugin frontend. Filters allow you to modify data, customize UI elements, and control client-side behavior.
---
## UI Elements
### `better_messages_header_element` {#better_messages_header_element}
Replaces or modifies the header element at the top of the threads list page. Return an HTML string or React JSX element. The `page` parameter identifies which page is rendering (currently `'index'`), and `mobileView` indicates if the mobile layout is active.
**Parameters:**
- `header` *(JSX.Element)* — Default IndexHeader component
- `page` *(string)* — Page identifier (`'index'`)
- `mobileView` *(boolean)* — Whether mobile view is active
**Returns:** `JSX.Element|string` — React component or HTML string
```js
wp.hooks.addFilter('better_messages_header_element', 'my_plugin', function(header, page, mobileView) {
if (mobileView) {
return 'Messages';
}
return header;
});
```

---
### `better_messages_footer_element` {#better_messages_footer_element}
Replaces or modifies the footer element at the bottom of the threads list page. Return an HTML string or React JSX element. Works the same as the header element filter with `page` and `mobileView` parameters.
**Parameters:**
- `footer` *(JSX.Element)* — Default ChatFooter component
- `page` *(string)* — Page identifier (`'index'`)
- `mobileView` *(boolean)* — Whether mobile view is active
**Returns:** `JSX.Element|string`
```js
wp.hooks.addFilter('better_messages_footer_element', 'my_plugin', function(footer, page, mobileView) {
if (page === 'index') {
return 'Add custom content';
}
return footer;
});
```

---
### `better_messages_thread_header_actions` {#better_messages_thread_header_actions}
Filters the complete list of action buttons in the conversation header — the right-side area with the call buttons, minimize/maximize controls and the "More" menu. The filter receives **every built-in button as an array item with a stable `key`**, so custom buttons can be inserted at any position, and built-in buttons can be reordered, removed, or replaced.
This is the supported way to customize the conversation header. Do not insert your own DOM node into `.thread-actions` — that container is rendered by React and re-created whenever the header re-renders, so a manually inserted element is repositioned or removed.
The same filter also runs for [mini chats](/docs/features/mini-widgets/); use the `isMiniChat` argument to tell the surfaces apart.
**Parameters:**
- `actions` *(array)* — The built-in header actions for the current conversation state
- `thread` *(object)* — Thread object
- `anotherUser` *(object|false)* — Other user in a one-to-one conversation, or `false`
- `isMiniChat` *(boolean)* — Whether the actions are being rendered in a mini chat header
**Returns:** `array`
**Built-in action keys** (each present only when the corresponding button applies to the current conversation and state):
| Surface | Keys, in default order |
|---|---|
| Conversation header | `online-users` (chat rooms, WebSocket version), `audio-call`, `video-call`, `group-audio-call`, `group-video-call`, `minimize` (to mini chat), `maximize`, `unmaximize` (restore from maximized), `more` (the ⋯ dropdown) |
| Mini chat, open | `video-call`, `audio-call`, `group-audio-call`, `group-video-call`, `minimize`, `close` |
| Mini chat, collapsed | `open`, `close` |
**Properties of each action:** `key` *(string, required — must be unique)*, `icon` *(string of SVG HTML, or a React node)*, `tooltip` *(string, optional)*, `classes` *(string, optional extra CSS classes)*, `onClick` *(function, optional)*, `onAuxClick` *(function, optional, middle-click)*. Built-in actions additionally carry `builtIn: true`, and the `more` item carries a pre-rendered `element` instead of an `icon` — pass it through unchanged unless you intend to remove it.
Adding a custom button before the window controls:
```js
wp.hooks.addFilter('better_messages_thread_header_actions', 'my_plugin', function(actions, thread, anotherUser, isMiniChat) {
var anchor = actions.findIndex(function(action) {
return ['minimize', 'maximize', 'unmaximize', 'more', 'close'].indexOf(action.key) !== -1;
});
var item = {
key: 'safety-menu',
tooltip: 'Safety options',
classes: 'my-safety-menu',
icon: '',
onClick: function(event) {
openMySafetyMenu(thread.thread_id, event.currentTarget);
}
};
if (anchor === -1) {
actions.push(item);
} else {
actions.splice(anchor, 0, item);
}
return actions;
});
```
Removing built-in buttons:
```js
wp.hooks.addFilter('better_messages_thread_header_actions', 'my_plugin', function(actions) {
return actions.filter(function(action) {
return action.key !== 'video-call' && action.key !== 'audio-call';
});
});
```
Replacing the behavior of a built-in button while keeping its position and icon:
```js
wp.hooks.addFilter('better_messages_thread_header_actions', 'my_plugin', function(actions, thread) {
var index = actions.findIndex(function(action) { return action.key === 'video-call'; });
if (index !== -1) {
actions[index] = Object.assign({}, actions[index], {
onClick: function() {
openMyCallFlow(thread.thread_id);
}
});
}
return actions;
});
```
Custom actions inherit the styling of the built-in header buttons — sizing, hover state and theme color are applied automatically, and your `classes` are added alongside `bm-thread-header-action` for further styling. Each button carries a `data-action-key` attribute matching its `key`, which is a stable anchor for a dropdown positioned against it.
:::info
Actions without a `key`, or without both an `icon` and an `element`, are skipped, and duplicate keys keep only the first occurrence. If your callback throws or returns something that is not an array, the error is logged to the browser console and the header falls back to the built-in actions rather than breaking.
:::
To add an entry to the existing "More" dropdown instead of a standalone button, use [`better_messages_thread_context_menu`](#better_messages_thread_context_menu).
---
### `better_messages_navigation_tabs` {#better_messages_navigation_tabs}
Filters the tabs of each navigation bar of the messenger: the [mini widgets bar](/docs/features/mini-widgets/), the side panel beside the conversations list (and its collapsed rail), and the tab bar at the bottom of the screen on phones. The filter receives **every tab of one bar, in order**, the built-in tabs included under stable keys, so you can add tabs, remove them, reorder them, rename them or give them another icon.
The admin's own [custom tabs](/docs/features/custom-tabs/) are in the list too, so a filter sees each bar exactly as it will be drawn.
:::info
Available since Better Messages 3.1.5.
:::
**Parameters:**
- `tabs` *(array)* — The tabs of the bar, in the order the admin set
- `surface` *(string)* — Which bar: `'mini'` (the mini widgets bar), `'side'` (the side panel and its collapsed rail) or `'mobile'` (the tab bar on phones)
**Returns:** `array`
**Built-in tab keys** (each present only when that tab is on for the bar and the member can see it):
| Key | Tab |
|---|---|
| `messages` | Conversations |
| `friends` | Friends |
| `groups` | Groups |
| `courses` | Courses |
| `ai-bots` | AI Bots |
| `chat-rooms` | Chat Rooms |
| `users` | Users |
The admin's custom tabs have keys that start with `ct_`.
**Properties of each tab:**
- `key` *(string, required)* — Unique on the bar. It is also the `data-tab` attribute of a tab you add, and the id the admin's tab order uses
- `label` *(string, required)* — Shown with the icon, or as a tooltip when the bar shows icons only
- `icon` *(string or React element, optional)* — A preset name from the icon picker (`comment`, `users`, `home`, `star`…), SVG markup, or an element. Leave it empty for the stock glyph
- `type` *(string)* — For a tab you add: `'link'` (goes to `url`), `'page'` (shows `url` in a frame inside the messenger) or `'custom'` (your own panel). Built-in tabs carry `'builtin'`, and the admin's content tabs `'content'`
- `url` *(string)* — The address of a `link` or `page` tab
- `newWindow` *(boolean, optional)* — Opens a `link` tab in a new window
- `badge` *(number or string, optional)* — A count or a short text over the icon. Zero shows nothing
- `render` *(function, optional)* — `render( container, context )` draws the panel of a `custom` tab into `container`, a plain DOM element, the first time the tab is opened. It may return a function, called when the panel goes. `context.surface` is the bar. `context.active` is `true` while the tab is in front and on screen and `false` once another tab comes to the front or the bar is shut: the object is the one `render` received, kept up to date, and the container carries the same as `data-active` (`"1"` or `"0"`). See [Know when your tab is in front](/docs/development/guides/custom-tabs/#know-when-your-tab-is-in-front). Since 3.2.0 for `context.active`
- `onClick` *(function, optional)* — `onClick( event, context )` runs on every press of the tab. A `custom` tab with `onClick` and no `render` never comes to the front, it only runs the click
- `guestWall` *(boolean, optional)* — `false` on a tab a visitor may open before signing in: the sign-in wall leaves that tab open to them and still covers the rest of the messenger. Ignored on a built-in tab. See [A tab a visitor can open before signing in](/docs/development/guides/custom-tabs/#a-tab-a-visitor-can-open-before-signing-in). Since 3.2.0
Built-in tabs also carry `builtIn: true`. You can change their `label`, `icon`, `badge` and `onClick`, or leave them out of the list. What a built-in tab shows stays the plugin's.
Adding a tab with a panel of your own:
```js
wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
tabs.push({
key: 'my-notes',
label: 'Notes',
icon: '',
type: 'custom',
render: function(container, context) {
container.innerHTML = 'Loading…';
var timer = setInterval(function() { refreshMyNotes(container); }, 30000);
refreshMyNotes(container);
return function() { clearInterval(timer); };
}
});
return tabs;
});
```
Renaming Conversations and removing the Users tab from the phone bar:
```js
wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
return tabs
.filter(function(tab) {
return ! (surface === 'mobile' && tab.key === 'users');
})
.map(function(tab) {
return tab.key === 'messages' ? Object.assign({}, tab, { label: 'Inbox' }) : tab;
});
});
```
A link to the course catalogue, first on the phone bar only:
```js
wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
if (surface !== 'mobile') return tabs;
return [{
key: 'catalogue',
label: 'Courses',
icon: 'home',
type: 'link',
url: '/courses/'
}].concat(tabs);
});
```
A tab whose badge follows something of your own: return the count from the filter, and ask the bars to run it again with [`better_messages_refresh_navigation_tabs`](/hooks/js-actions#better_messages_refresh_navigation_tabs) when the count changes.
```js
var pendingTasks = 0;
wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs) {
return tabs.map(function(tab) {
return tab.key === 'messages' ? Object.assign({}, tab, { badge: pendingTasks }) : tab;
});
});
function onTasksChanged(count) {
pendingTasks = count;
wp.hooks.doAction('better_messages_refresh_navigation_tabs');
}
```
The bars run the filter again by themselves when a filter is added or removed, so a script that loads after the messenger has drawn works as well.
A `custom` tab added on the server with [`better_messages_custom_tabs`](/hooks/php-filters#better_messages_custom_tabs) comes to the filter with no `render`. Give it one by its key, and it shows on the bars the server chose. Without a `render` or an `onClick` it stays off them. Since 3.2.0.
```js
wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs) {
return tabs.map(function(tab) {
if (tab.key !== 'my-chat') return tab;
return Object.assign({}, tab, {
render: function(container, context) { return myChat.mount(container, context); }
});
});
});
```
To bring a tab to the front from your own code, run [`better_messages_open_navigation_tab`](/hooks/js-actions#better_messages_open_navigation_tab) with its key.
:::info
Tabs without a `key` or a `label`, a `link` or `page` tab without a `url`, and a `custom` tab with neither `render` nor `onClick` are skipped, and duplicate keys keep only the first occurrence. A `content` tab can only be one the site published, as its content is rendered by the site. If your callback throws or returns something that is not an array, the error is logged to the browser console and the bar falls back to its own tabs rather than breaking. An error inside `render` or `onClick` is logged the same way.
:::
To add tabs on the server, for the members who should see them, use the PHP filter [`better_messages_custom_tabs`](/hooks/php-filters#better_messages_custom_tabs).
---
### `better_messages_messages_list_before` {#better_messages_messages_list_before}
Adds HTML content above the messages list, before the message history begins. The HTML is parsed and rendered as React elements. Return an empty string to show nothing. Useful for thread-specific announcements, encryption status banners, or custom info boxes.
**Parameters:**
- `content` *(string)* — Default empty string
- `thread` *(object)* — Thread object
**Returns:** `string` — HTML string
```js
wp.hooks.addFilter('better_messages_messages_list_before', 'my_plugin', function(content, thread) {
if (thread.type === 'group') {
return 'Welcome to the group chat!';
}
return content;
});
```

---
### `better_messages_send_button_after` {#better_messages_send_button_after}
Adds HTML content after the send button in the reply form area. The HTML is rendered inline with the send button. Useful for adding custom action buttons, emoji shortcuts, or addon-specific controls next to the message composer.
**Parameters:**
- `content` *(string)* — Default empty string
- `thread` *(object)* — Thread object
**Returns:** `string` — HTML string
```js
wp.hooks.addFilter('better_messages_send_button_after', 'my_plugin', function(content, thread) {
return '';
});
```

---
### `better_messages_participants_list_after` {#better_messages_participants_list_after}
Adds HTML content after the participants list in thread information panel.
**Parameters:**
- `content` *(string)* — Default empty string
- `thread` *(object)* — Thread object
**Returns:** `string` — HTML string
```js
wp.hooks.addFilter('better_messages_participants_list_after', 'my_plugin', function(html, thread) {
html += 'Add custom data';
return html;
});
```

---
### `better_messages_user_settings_after` {#better_messages_user_settings_after}
Adds custom settings sections after the user settings options.
**Parameters:**
- `elements` *(array)* — Default empty array
**Returns:** `array` — Array of React elements
---
## Guest Authentication Screen
The guest authentication screen appears when a visitor without a WordPress account or guest record tries to start a conversation. By default it shows two stacked sections — **Continue with account** (Login / Register) and **Continue as guest** (display-name form). It is rendered in three places:
- **Modal popup** — when a guest clicks a chat button (live chat, mini-widget "Start a new conversation", chat-room "Join", user / AI bot list item)
- **Mini-widget banner** — inside the floating mini-widget when a guest opens it
- **Full messages page wall** — on the main `/messages/` page (or wherever the messages app is embedded)
The filters below let you inject arbitrary HTML at three positions inside the screen without overriding the component. They fire in **all three locations automatically** because the underlying `AuthScreen` component is shared. Each filter receives the current HTML output (empty string by default) and an `args` object describing which sections are visible. Concatenate your HTML to the input and return the new string; return the input unchanged for no injection. Filters from multiple plugins compose naturally because each receives the previous filter's output.
:::info
The filters work with both the free and WebSocket builds. The injected HTML is rendered into the screen verbatim, so you can include images, links, lists, or any markup. Style your injected content with CSS that targets your custom classes.
:::

The same filters fire inside the mini-widget banner and the full messages page wall:

### `better_messages_auth_required_top` {#better_messages_auth_required_top}
Injects content **above** the "Continue with account" and "Continue as guest" sections. Useful for a welcome message, marketing intro, or context about who the visitor is about to chat with.
**Parameters:**
- `output` *(string)* — Empty by default; concatenate to it (`output + '…'`) to compose with content from other filters
- `args` *(object)* — `{ showLogin: boolean, showGuestSubHeader: boolean, forceGuest: boolean }`
**Returns:** `string` — HTML string to inject (return the input unchanged for no injection)
```js
wp.hooks.addFilter('better_messages_auth_required_top', 'my_plugin', function (output, args) {
return output + '' +
'Welcome to our store chat' +
'Sign in to track your orders or continue as a guest.' +
'';
});
```
---
### `better_messages_auth_required_between` {#better_messages_auth_required_between}
Injects content **between** the "Continue with account" section and the "Continue as guest" section. Useful for a divider, a "— OR —" label, or contextual help that should sit between the two auth choices.
**Parameters:**
- `output` *(string)*
- `args` *(object)* — `{ showLogin: boolean, showGuestSubHeader: boolean, forceGuest: boolean }`
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_auth_required_between', 'my_plugin', function (output) {
return output + 'or';
});
```
---
### `better_messages_auth_required_bottom` {#better_messages_auth_required_bottom}
Injects content **below** the "Continue as guest" section. Useful for legal disclosures, links to your privacy policy, support contacts, or any closing text the visitor should see before chatting.
**Parameters:**
- `output` *(string)*
- `args` *(object)* — `{ showLogin: boolean, showGuestSubHeader: boolean, forceGuest: boolean }`
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_auth_required_bottom', 'my_plugin', function (output) {
return output + '' +
'By continuing you agree to our Privacy Policy.' +
'';
});
```
---
### Layout tips
The default modal stacks the login section above the guest section vertically. If you want them side-by-side (and your custom content full-width above), use CSS Grid on the modal wrapper:
```css
.bm-modal-window.bm-modal-guest-settings-panel .bm-auth-required-modal {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
align-items: start;
}
.bm-modal-window.bm-modal-guest-settings-panel .bm-pre-chat-intro,
.bm-modal-window.bm-modal-guest-settings-panel .my-auth-footnote { grid-column: 1 / -1; }
.bm-modal-window.bm-modal-guest-settings-panel .bm-guest-login { grid-column: 1; }
.bm-modal-window.bm-modal-guest-settings-panel .bm-guest-form { grid-column: 2; }
.bm-modal-window.bm-modal-guest-settings-panel .bm-guest-login-actions { display: flex; flex-direction: column; gap: 8px; }
.bm-modal-window.bm-modal-guest-settings-panel .bm-modal-window-content { overflow-y: auto; }
```
Replace `.bm-pre-chat-intro` and `.my-auth-footnote` with whatever root class names your injected HTML uses.
---
## Message Styling & Context Menu
### `better_messages_single_message_class` {#better_messages_single_message_class}
Adds custom CSS classes to individual message elements in the message list. The `message` object contains all message properties (sender_id, thread_id, content, etc.) so you can conditionally style messages based on sender, content, or metadata.
**Parameters:**
- `className` *(string)* — Current CSS classes
- `message` *(object)* — Message object
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_single_message_class', 'my_plugin', function(className, message) {
if (message.sender_id === 1) {
className += ' admin-message';
}
return className;
});
```
---
### `better_messages_single_message_context_menu` {#better_messages_single_message_context_menu}
Modifies the context menu items shown when right-clicking or long-pressing a message. Each menu item should have `key`, `label`, `icon` (SVG string), and `action` (callback function) properties. You can add, remove, or reorder items in the array.
**Parameters:**
- `items` *(array)* — Array of MessageContextMenuItem objects
- `message` *(object)* — Message object
- `thread` *(object)* — Thread object
**Returns:** `array`
**Available properties for menu items:** `key` *(string, required for React)*, `label` *(string)*, `icon` *(string, SVG HTML)*, `action` *(function, callback on click)*.
```js
wp.hooks.addFilter('better_messages_single_message_context_menu', 'my_plugin', function(items, message, thread) {
items.push({
key: 'context-report-item',
icon: '',
label: 'Report',
action: function() {
alert('Reported message #' + message.message_id);
}
});
return items;
});
```

---
### `better_messages_video_preload_attribute` {#better_messages_video_preload_attribute}
Controls the `preload` attribute for video players in messages.
**Parameters:**
- `preload` *(string)* — Default `'auto'`
**Returns:** `string` — `'none'`, `'metadata'`, or `'auto'`
```js
wp.hooks.addFilter('better_messages_video_preload_attribute', 'my_plugin', function() {
return 'metadata'; // Save bandwidth
});
```
---
## Thread Styling & Context Menu
### `better_messages_single_thread_class` {#better_messages_single_thread_class}
Adds custom CSS classes to individual thread items in the threads list sidebar. The `thread` object contains all thread properties (thread_id, type, subject, unread_count, etc.) so you can conditionally style threads based on state or metadata.
**Parameters:**
- `className` *(string)* — Current CSS classes
- `thread` *(object)* — Thread object
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_single_thread_class', 'my_plugin', function(className, thread) {
if (thread.pinned) {
className += ' pinned-thread';
}
return className;
});
```
---
### `better_messages_thread_context_menu` {#better_messages_thread_context_menu}
Modifies the context menu actions shown when right-clicking or long-pressing a thread in the threads list. Each action should have `key`, `label`, `tooltip`, `icon`, `classes`, `showInThread`, `showInList`, `onClick`, and optionally `onAuxClick` properties.
**Parameters:**
- `actions` *(array)* — Array of ThreadAction objects
- `thread` *(object)* — Thread object
- `anotherUser` *(object|false)* — Other user in PM, or `false`
- `isMiniChat` *(boolean)* — Whether in mini chat mode
**Returns:** `array`
**Available properties for actions:** `key` *(string, required for React)*, `label` *(string)*, `tooltip` *(string)*, `icon` *(string, SVG HTML)*, `classes` *(string, CSS classes)*, `showInThread` *(bool)*, `showInList` *(bool)*, `onClick` *(function)*, `onAuxClick` *(function, middle-click)*.
```js
wp.hooks.addFilter('better_messages_thread_context_menu', 'my_plugin', function(buttons, thread, anotherUser, isMiniChat) {
buttons.push({
key: 'custom-action',
showInThread: true,
showInList: true,
tooltip: 'Archive this conversation',
label: 'Archive',
icon: '',
classes: 'bm-archive-action',
onClick: function() {
alert('Archive thread #' + thread.thread_id);
}
});
return buttons;
});
```

To add a standalone button to the conversation header instead of an entry in this menu, use [`better_messages_thread_header_actions`](#better_messages_thread_header_actions).
---
### `better_messages_threads_list_selector` {#better_messages_threads_list_selector}
Filters the database query selector for the threads list.
**Parameters:**
- `selector` *(object)* — Default: `{ isDeleted: 0, isHidden: 0, adminAccess: { $exists: false } }`
**Returns:** `object`
```js
wp.hooks.addFilter('better_messages_threads_list_selector', 'my_plugin', function(selector) {
// Only show group threads
selector.type = 'group';
return selector;
});
```
---
## User Elements
### `better_messages_display_name_allowed_html` {#better_messages_display_name_allowed_html}
Filters the HTML a display name is allowed to carry, in the browser.
A display name is rendered as markup rather than printed as text, which is what lets an integration hang a badge or a rank icon off one. The name is also whatever the member typed into their own profile, so the messenger filters every name through this allowlist before rendering it — including a name it loaded from local storage or received over the realtime connection, neither of which passes through the server on the way in. The allowlist arrived as a security improvement in 3.0.4.
This is the browser half of [the PHP filter of the same name](/hooks/php-filters/#better_messages_display_name_allowed_html). Both lists carry the hook, under the same name and the same shape, and a tag has to be added to both to render everywhere — PHP alone covers wp-admin and server-rendered surfaces, and the messenger still drops it.
**Parameters:**
- `allowed` *(object)* — Tag name to allowed-attribute map. A key ending in `*` matches by prefix, so `'data-*': true` admits every data attribute
**Returns:** `object`
```js
wp.hooks.addFilter(
'better_messages_display_name_allowed_html',
'my_plugin',
function ( allowed ) {
allowed.ins = { 'class': true, 'data-*': true };
return allowed;
}
);
```
Enqueue your script with `wp-hooks` as a dependency so the filter registers before the messenger renders its first name.
:::warning Some things stay refused
Event-handler attributes (`on*`), `srcdoc` and `formaction` are stripped whatever this filter returns, and so are scripting and framing elements — `iframe`, `script`, `object`, `embed`, `form`, `svg`, `math` and the rest. A display name is attacker-controlled on any site where a member can edit their own profile, and putting those back would undo the security improvement this allowlist is. No filter can re-enable them.
:::
---
### `better_messages_avatar_class` {#better_messages_avatar_class}
Adds custom CSS classes to user avatar elements throughout the UI. The default includes `'avatar bbpm-avatar'` and an `'online'` class when the user is online. The `user` object provides user data for conditional styling.
**Parameters:**
- `className` *(string)* — Default `'avatar bbpm-avatar'` + online status
- `user` *(object)* — User object
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_avatar_class', 'my_plugin', function(className, user) {
if (user.verified) {
className += ' verified-avatar';
}
return className;
});
```
---
### `better_messages_avatar_attributes` {#better_messages_avatar_attributes}
Adds custom HTML attributes to avatar elements.
**Parameters:**
- `attributes` *(object)* — Default empty object
- `user` *(object)* — User object
**Returns:** `object`
```js
wp.hooks.addFilter('better_messages_avatar_attributes', 'my_plugin', function(attrs, user) {
attrs['data-role'] = user.role || 'member';
return attrs;
});
```
---
### `better_messages_before_avatar_click` {#better_messages_before_avatar_click}
Intercepts a click on a member's avatar, or on a mention of them in a message (since 3.0.9), before the default profile navigation occurs. Return `true` to prevent the default behavior and handle the click yourself (e.g., open a custom profile modal). Return `false` to allow normal navigation.
**Parameters:**
- `prevented` *(boolean)* — Default `false`
- `user_id` *(number)* — User ID
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_before_avatar_click', 'my_plugin', function(prevented, userId) {
// Open custom profile modal instead of navigating
openProfileModal(userId);
return true; // Prevent default navigation
});
```
---
### `better_messages_username_class` {#better_messages_username_class}
Adds custom CSS classes to username elements.
**Parameters:**
- `className` *(string)* — Default `'user bm-user'`
- `user` *(object)* — User object
**Returns:** `string`
```js
wp.hooks.addFilter('better_messages_username_class', 'my_plugin', function(classes, user) {
classes += ' notranslate';
return classes;
});
```
---
### `better_messages_username_attributes` {#better_messages_username_attributes}
Adds custom HTML attributes to username elements.
**Parameters:**
- `attributes` *(object)* — Default empty object
- `user` *(object)* — User object
**Returns:** `object`
---
### `better_messages_before_username_click` {#better_messages_before_username_click}
Intercepts username click events before the default profile navigation occurs. Return `true` to prevent the default behavior and handle the click yourself. Return `false` to allow normal navigation to the user's profile.
**Parameters:**
- `prevented` *(boolean)* — Default `false`
- `user_id` *(number)* — User ID
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_before_username_click', 'my_plugin', function(prevented, userId) {
openProfileModal(userId);
return true;
});
```
---
## Message Sending
### `better_messages_before_message_sent` {#better_messages_before_message_sent}
Prevents message sending or performs custom validation before a message is sent. Return `true` to block the message from being sent (the user stays in the composer). Return `false` to allow sending to proceed normally.
**Parameters:**
- `prevented` *(boolean)* — Default `false`
- `threadId` *(number)* — Thread ID
- `thread` *(object)* — Thread object
**Returns:** `boolean` — `true` to prevent, `false` to allow
```js
wp.hooks.addFilter('better_messages_before_message_sent', 'my_plugin', function(prevented, threadId, thread) {
if (!confirm('Are you sure you want to send this message?')) {
return true; // Prevent sending
}
return false;
});
```
---
## API & Network
### `better_messages_api_request_config` {#better_messages_api_request_config}
Modifies the axios request configuration before any Better Messages REST API call is made. The config object includes URL, headers, data, and method. Use this to add custom headers, modify URLs, or inject authentication tokens for external auth systems.
**Parameters:**
- `config` *(object)* — Axios request config (URL, headers, data, etc.)
**Returns:** `object`
```js
wp.hooks.addFilter('better_messages_api_request_config', 'my_plugin', function(config) {
config.headers['X-Custom-Header'] = 'my-value';
return config;
});
```
---
### `better_messages_api_response` {#better_messages_api_response}
Filters API responses before they are processed by the application. Receives the full axios response object including status, data, and headers. Use this to transform response data, handle custom headers, or log API interactions.
**Parameters:**
- `response` *(object)* — Axios response object
**Returns:** `object`
```js
wp.hooks.addFilter('better_messages_api_response', 'my_plugin', function(response) {
// Transform or log response data
console.log('API response:', response.config.url, response.status);
return response;
});
```
---
### `better_messages_unhandled_api_error` {#better_messages_unhandled_api_error}
Handles unhandled API errors. Return a new config to retry, or `false` to reject.
**Parameters:**
- `retry` *(boolean)* — Default `false`
- `response` *(object)* — Error response
- `config` *(object)* — Original request config
**Returns:** `object|false`
```js
wp.hooks.addFilter('better_messages_unhandled_api_error', 'my_plugin', function(retry, response, config) {
if (response.status === 401) {
// Redirect to login
window.location.href = '/login';
}
return false;
});
```
---
## Unread Counts
### `better_messages_total_unread` {#better_messages_total_unread}
Filters the unread count the page shows as the member's total: the page title, every `.bm-menu-unread` counter, the count on the mini widgets bubble and the [`better_messages_update_unread`](/hooks/js-actions#better_messages_update_unread) action. A plugin with unread messages of its own, shown in a navigation tab of its own say, adds them here. The Messages tab keeps Better Messages' own count, so nothing is counted twice. Since 3.2.0.
When your count changes, run [`better_messages_refresh_total_unread`](/hooks/js-actions#better_messages_refresh_total_unread). A filter added after the page has drawn its counters is picked up at once. The button that opens the messenger on a phone is printed by the site with the count it has then: add yours there with the PHP filter of the same name, [`better_messages_total_unread`](/hooks/php-filters#better_messages_total_unread). See [Add your unread messages to the total](/docs/development/guides/custom-tabs/#add-your-unread-messages-to-the-total).
**Parameters:**
- `count` *(number)* — Better Messages' own unread count
**Returns:** `number` — rounded down. A negative value or one that is not a number shows Better Messages' own count, and so does an error in your callback, which is logged to the browser console
```js
var myUnread = 0;
wp.hooks.addFilter('better_messages_total_unread', 'my_plugin', function(count) {
return count + myUnread;
});
document.addEventListener('my-chat-unread', function(event) {
myUnread = event.detail.count;
wp.hooks.doAction('better_messages_refresh_total_unread');
});
```
---
## Navigation
### `better_messages_navigate_url` {#better_messages_navigate_url}
Intercepts URL navigation when opening conversations. Return `true` if you handle navigation yourself.
**Parameters:**
- `handled` *(boolean)* — Default `false`
- `url` *(string)* — Target URL
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_navigate_url', 'my_plugin', function(handled, url) {
// Use SPA router instead of full page reload
myRouter.push(url);
return true;
});
```
---
## File Uploads
### `better_messages_uploader_enabled` {#better_messages_uploader_enabled}
Controls whether file drag-and-drop and paste upload functionality is enabled globally. Return `false` to completely disable file uploads in the UI. When disabled, the upload button and drag-drop zones are removed.
**Parameters:**
- `enabled` *(boolean)* — Default `true`
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_uploader_enabled', 'my_plugin', function() {
return false; // Disable file uploads
});
```
---
### `better_messages_open_uploader_modal` {#better_messages_open_uploader_modal}
Controls whether the file uploader modal opens.
**Parameters:**
- `open` *(boolean)* — Default `true`
- `buttonRef` *(HTMLElement|null)* — Upload button reference
**Returns:** `boolean`
---
## Calls
### `better_messages_call_show_notification` {#better_messages_call_show_notification}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Controls whether to show the incoming call notification.
**Parameters:**
- `show` *(boolean)* — Default `true`
- `message` *(object)* — Incoming call data
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_call_show_notification', 'my_plugin', function(show, message) {
// Suppress notification for specific threads
if (message.thread_id === 123) return false;
return show;
});
```
---
### `better_messages_call_show_incoming_screen` {#better_messages_call_show_incoming_screen}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Controls whether to show the incoming call screen.
**Parameters:**
- `show` *(boolean)* — Default `true`
- `data` *(object)* — Incoming call data
**Returns:** `boolean`
---
### `better_messages_call_answer_force` {#better_messages_call_answer_force}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Indicates whether a call was answered by an external system (e.g., VoIP).
**Parameters:**
- `answered` *(boolean)* — Default `false`
- `messageId` *(number)* — Call message ID
- `threadId` *(number)* — Thread ID
**Returns:** `boolean`
---
### `better_messages_call_create_not_allowed` {#better_messages_call_create_not_allowed}
Controls whether to show error when call creation is denied.
**Parameters:**
- `showError` *(boolean)* — Default `true`
- `thread_id` *(number)* — Thread ID
- `type` *(string)* — `'audio'` or `'video'`
**Returns:** `boolean`
---
### `better_messages_call_join_not_allowed` {#better_messages_call_join_not_allowed}
Controls whether to show error when joining a call is denied.
**Parameters:**
- `showError` *(boolean)* — Default `true`
- `thread_id` *(number)* — Thread ID
- `type` *(string)* — `'audio'` or `'video'`
**Returns:** `boolean`
---
### `better_messages_outgoing_call_not_allowed` {#better_messages_outgoing_call_not_allowed}
Controls whether to show error when starting an outgoing call is denied.
**Parameters:**
- `showError` *(boolean)* — Default `true`
- `user_id` *(number)* — Recipient user ID
- `thread_id` *(number)* — Thread ID
- `type` *(string)* — `'audio'` or `'video'`
**Returns:** `boolean`
---
### `better_messages_group_video_call_items` {#better_messages_group_video_call_items}
:::info WebSocket Version
This functionality is available only with the WebSocket version.
:::
Filters the map of participants displayed in group video calls.
**Parameters:**
- `items` *(Map)* — Map of participant identity to GroupVideoParticipantData
- `thread` *(object)* — Thread object
**Returns:** `Map`
---
## Miscellaneous
### `better_messages_max_toasts_limit` {#better_messages_max_toasts_limit}
Limits the maximum number of toast notifications (on-site popups for new messages) displayed at the same time. Return `null` for unlimited toasts, or a number to cap them. Older toasts are removed when the limit is exceeded.
**Parameters:**
- `limit` *(null|number)* — Default `null` (no limit)
**Returns:** `null|number`
```js
wp.hooks.addFilter('better_messages_max_toasts_limit', 'my_plugin', function() {
return 3; // Max 3 toasts
});
```

---
### `better_messages_emoji_important` {#better_messages_emoji_important}
Controls whether the `!important` flag is applied to emoji CSS styles. Default is `true` to ensure emojis render correctly regardless of theme CSS. Return `false` if emoji styles conflict with your theme and you want to manage emoji sizing yourself.
**Parameters:**
- `useImportant` *(boolean)* — Default `true`
**Returns:** `boolean`
```js
wp.hooks.addFilter('better_messages_emoji_important', 'my_plugin', function() {
return false; // Remove !important from emoji styles
});
```
---
### `better_messages_no_sleep_enable` {#better_messages_no_sleep_enable}
Controls whether screen lock prevention activates during calls.
**Parameters:**
- `enable` *(boolean)* — Default `true`
**Returns:** `boolean`
---
### `better_messages_no_sleep_disable` {#better_messages_no_sleep_disable}
Controls whether screen lock prevention deactivates after calls.
**Parameters:**
- `disable` *(boolean)* — Default `true`
**Returns:** `boolean`
---
## Better Messages JavaScript Functions Reference — BetterMessages API
# JavaScript Functions Reference
:::info REQUIREMENTS
All functions are available via the global `BetterMessages` object. Add your code to a custom JavaScript file enqueued after Better Messages, or use inline `