Skip to main content

Custom navigation tabs from code

The messenger has three navigation bars: the mini widgets bar (mini), the side panel beside the conversations list, with its collapsed rail (side), and the tab bar on phones (mobile). An admin adds tabs of their own and renames the built-in ones at Settings → Appearance, as Custom tabs describes. This guide is for doing the same, and more, from code.

There are two places to do it:

WhereFilterUse it to
In the browserbetter_messages_navigation_tabsChange any tab of any bar, the built-in ones included. Add a tab that draws its own panel, runs a click or carries a badge
On the serverbetter_messages_custom_tabsAdd a tab only for the members who should have it, by what the site knows about them: a membership, a capability, an enrolment

Both are available since Better Messages 3.1.5.

The JavaScript filter#

The filter runs once for each bar and receives that bar's tabs in order:

wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
// tabs: [{ key: 'messages', label: 'Messages', builtIn: true, type: 'builtin', ... }, ...]
// surface: 'mini', 'side' or 'mobile'
return tabs;
});

Load your script after wp-hooks, as for any Better Messages JavaScript hook. A filter added after the messenger has drawn is picked up at once, so the order of loading does not matter beyond that.

The built-in tabs have the keys messages, friends, groups, courses, ai-bots, chat-rooms and users, each present only when it is on for the bar and the member can see it. The admin's custom tabs have keys starting with ct_. The reference lists every property of a tab.

Rename, re-icon, reorder or remove a tab#

Return the tabs changed. A built-in tab keeps what it shows, whatever its label or icon:

wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
var renamed = tabs.map(function(tab) {
if (tab.key === 'messages') return Object.assign({}, tab, { label: 'Inbox', icon: 'mail' });
if (tab.key === 'chat-rooms') return Object.assign({}, tab, { label: 'Lounges' });
return tab;
});

// Chat rooms first on the phone, Users nowhere on it.
if (surface === 'mobile') {
renamed = renamed.filter(function(tab) { return tab.key !== 'users'; });
renamed.sort(function(a, b) { return (b.key === 'chat-rooms') - (a.key === 'chat-rooms'); });
}

return renamed;
});

The first tab of a bar is the one the messenger opens on, so moving a tab first in the filter also decides that, as dragging it first in the admin does.

wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
tabs.push({
key: 'help',
label: 'Help',
icon: 'star',
type: 'link',
url: '/help/',
newWindow: true
});

tabs.push({
key: 'events',
label: 'Events',
icon: 'home',
type: 'page',
url: '/events/'
});

return tabs;
});

A page tab shows the address in a frame inside the messenger. A page of your own site opens its links in the whole window. A page of another site shows only if that site allows being framed.

Add a tab with a panel of your own#

Give the tab a render function. It is called with an empty element the first time the tab is opened, and the element stays on screen, hidden while another tab is in front, until the messenger goes. Return a function to clean up after yourself:

wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs, surface) {
tabs.push({
key: 'my-leaderboard',
label: 'Leaderboard',
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 21V11M16 21V7M12 21V3"/></svg>',
type: 'custom',
render: function(container, context) {
container.innerHTML = '<div class="my-leaderboard">Loading…</div>';

var load = function() {
fetch('/wp-json/my-plugin/v1/leaderboard', { credentials: 'same-origin' })
.then(function(response) { return response.json(); })
.then(function(rows) {
container.innerHTML = '<ol class="my-leaderboard">' + rows.map(function(row) {
return '<li>' + row.name + ' <b>' + row.points + '</b></li>';
}).join('') + '</ol>';
});
};

load();
var timer = setInterval(load, 60000);

return function() { clearInterval(timer); };
}
});

return tabs;
});

The container scrolls by itself when what you draw is taller than the panel. Plain DOM is enough, the messenger does not need to know how you draw. Style your panel with your own classes. The messenger's colours are available as CSS variables, see the CSS reference.

A tab with an onClick and no render never comes to the front, it only runs the click. Use it for a tab that opens something of yours, a modal say:

tabs.push({
key: 'new-ticket',
label: 'New ticket',
icon: 'edit',
type: 'custom',
onClick: function(event, context) { openMyTicketForm(); }
});

Keep a badge current#

A tab shows badge over its icon: a number (zero shows nothing) or a short text. Return the current value from the filter, and ask the bars to run it again when it changes:

var openTickets = 0;

wp.hooks.addFilter('better_messages_navigation_tabs', 'my_plugin', function(tabs) {
return tabs.map(function(tab) {
return tab.key === 'new-ticket' ? Object.assign({}, tab, { badge: openTickets }) : tab;
});
});

document.addEventListener('my-tickets-changed', function(event) {
openTickets = event.detail.count;
wp.hooks.doAction('better_messages_refresh_navigation_tabs');
});

When something goes wrong#

A tab without a key or a label, a link or page tab without a url, and a custom tab with neither render nor onClick are left out, and only the first of two tabs with the same key is kept. If the filter throws, or returns something that is not an array, the bar shows its own tabs and the error is logged to the browser console. An error in render or onClick is logged too, and the rest of the messenger carries on.

The PHP filter#

The JavaScript filter decides what a tab looks like in the browser, but everyone who loads the page runs it. To give a tab only to some members, add it on the server:

<?php
add_filter( 'better_messages_custom_tabs', 'my_staff_tab', 10, 2 );
function my_staff_tab( $tabs, $user_id ){
if ( $user_id <= 0 || ! user_can( $user_id, 'edit_posts' ) ) {
return $tabs;
}

$tabs[] = array(
'key' => 'staff-room',
'label' => 'Staff room',
'icon' => 'users',
'type' => 'content',
'bars' => array( 'side', 'mobile' ),
'content' => '<p>Rota, handbook and contacts:</p>[my_staff_links]',
);

return $tabs;
}

The filter receives the admin's custom tabs, already without the ones hidden from the member's role, so you can also change or remove those. $user_id is the member, negative for a guest and 0 for a visitor who has not started chatting as a guest yet.

A content tab's content is rendered by the site when the member opens the tab, with its shortcodes run for that member, and is never part of the page. The same filter runs when the tab is opened, so a member who does not get the tab from your filter cannot ask for its content either.

For content you build in PHP, give a content_callback instead of content:

<?php
add_filter( 'better_messages_custom_tabs', 'my_orders_tab', 10, 2 );
function my_orders_tab( $tabs, $user_id ){
if ( $user_id <= 0 || ! function_exists( 'wc_get_orders' ) ) {
return $tabs;
}

$tabs[] = array(
'key' => 'my-orders',
'label' => 'Orders',
'icon' => 'inbox',
'type' => 'content',
'bars' => array( 'mini', 'side', 'mobile' ),
'content_callback' => function( $tab, $user_id ) {
$orders = wc_get_orders( array( 'customer_id' => $user_id, 'limit' => 5 ) );
if ( empty( $orders ) ) {
return '<p>No orders yet.</p>';
}

$html = '<ul>';
foreach ( $orders as $order ) {
$html .= '<li><a href="' . esc_url( $order->get_view_order_url() ) . '">#' . esc_html( $order->get_order_number() ) . '</a> ' . esc_html( wc_get_order_status_name( $order->get_status() ) ) . '</li>';
}

return $html . '</ul>';
},
);

return $tabs;
}

To change the content of any content tab after it is rendered, use better_messages_navigation_tab_content.

A tab added in PHP comes to the browser like the admin's tabs, so the JavaScript filter sees it too. It is not listed in the admin's bar lists, and it comes at the end of each bar it is on. To place it elsewhere, move it in the JavaScript filter.

Testing checklist#

  • Each bar you added the tab to shows it, and the others do not: the mini widgets bar on a wide screen, the side panel on the messages page, the tab bar on a phone
  • A member without the tab, and a guest, do not get it, and /wp-json/better-messages/v1/navigation/tabs/<key>/content answers 404 for them when it is a content tab
  • The panel looks right in the light and the dark scheme, and with the bar showing icons only
  • A filter that throws in your code still leaves the messenger working