J’essayais de rendre le chat un peu plus convivial et de faciliter la recherche du nombre de personnes dans le canal de discussion. Jusqu’à présent, j’y suis parvenu avec ce composant de thème au style « hacky » (sous licence CC-0 si quelqu’un le trouve utile).
Code source de l'extension
CSS
.chat-channel-member-count {
color: var(--primary-medium);
.d-icon { display: inline-flex; }
&:hover { color: var(--primary-high); text-decoration: none; }
}
JS
// javascripts/discourse/api-initializers/chat-member-count.js
import { apiInitializer } from "discourse/lib/api";
import { ajax } from "discourse/lib/ajax";
import { iconHTML } from "discourse-common/lib/icon-library";
import DiscourseURL from "discourse/lib/url"; // <- SPA router
export default apiInitializer("1.8.0", (api) => {
let badgeEl = null;
let mo = null;
let currentId = null;
const pathMatch = () => window.location.pathname.match(/\/chat\/c\/([^/]+)\/(\d+)/);
const getSlugId = () => {
const m = pathMatch();
return m ? { slug: m[1], id: parseInt(m[2], 10) } : null;
};
const titleEl = () => document.querySelector(".chat-channel-title");
const makeHref = ({ slug, id }) => `/chat/c/${slug}/${id}/info/members`;
async function waitForTitle(maxMs = 10000) {
if (titleEl()) return titleEl();
const start = performance.now();
return new Promise((resolve) => {
const obs = new MutationObserver(() => {
if (titleEl() || performance.now() - start > maxMs) {
obs.disconnect();
resolve(titleEl() || null);
}
});
obs.observe(document.body, { childList: true, subtree: true });
});
}
async function fetchMemberCount(id) {
try {
const ch = await ajax(`/chat/api/channels/${id}`);
const n =
ch?.user_count ??
ch?.memberships_count ??
ch?.users_count ??
ch?.stats?.members_count;
if (Number.isFinite(n)) return n;
} catch {}
try {
const res = await ajax(`/chat/api/channels/${id}/memberships`);
const metaTotal = res?.meta?.total ?? res?.total;
if (Number.isFinite(metaTotal)) return metaTotal;
return Array.isArray(res?.memberships) ? res.memberships.length : null;
} catch {
return null;
}
}
function renderBadge(href, count) {
const a = document.createElement("a");
a.href = href;
a.className = "chat-channel-member-count";
a.title = "Channel members";
a.setAttribute("data-auto-route", "true"); // cosmetic; we handle routing below
Object.assign(a.style, {
marginLeft: ".5rem",
display: "inline-flex",
alignItems: "center",
gap: ".25rem",
pointerEvents: "auto",
position: "relative",
zIndex: "2",
});
// Left click -> SPA route; modifier clicks behave normally
a.addEventListener("click", (e) => {
const modified = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
if (modified) return; // let browser open new tab/window
e.preventDefault();
e.stopPropagation(); // don't open channel settings
DiscourseURL.routeTo(href); // <- SPA navigation (no reload)
});
const iconWrap = document.createElement("span");
iconWrap.className = "d-icon";
iconWrap.innerHTML = iconHTML("user");
const num = document.createElement("span");
num.textContent = Number.isFinite(count) ? String(count) : "–";
a.appendChild(iconWrap);
a.appendChild(num);
return a;
}
function removeBadge() {
badgeEl?.remove?.();
badgeEl = null;
}
async function mount() {
const si = getSlugId();
if (!si) return;
const el = await waitForTitle();
if (!el) return;
removeBadge();
const count = await fetchMemberCount(si.id);
badgeEl = renderBadge(makeHref(si), count);
el.insertAdjacentElement("afterend", badgeEl);
// Keep badge if header re-renders
mo?.disconnect?.();
mo = new MutationObserver(() => {
if (badgeEl && !document.body.contains(badgeEl)) {
const t = titleEl();
if (t) t.insertAdjacentElement("afterend", badgeEl);
}
});
mo.observe(el.closest(".chat-header, body"), { childList: true, subtree: true });
}
function teardown() {
removeBadge();
mo?.disconnect?.();
mo = null;
}
// React to route changes
api.onPageChange(() => {
const si = getSlugId();
const id = si?.id || null;
if (id && id !== currentId) {
currentId = id;
mount();
} else if (!id) {
currentId = null;
teardown();
}
});
// Initial render if already on a channel
const first = getSlugId();
if (first) {
currentId = first.id;
mount();
}
});
Résultat :

Je me demande cependant si cela a du sens en tant qu’implémentation par défaut. Il est plus facile à découvrir que :
- Cliquer sur l’en-tête du canal
- Cliquer sur l’onglet des membres
De plus, avec le compteur de membres en place, on pourrait supprimer les onglets des paramètres puisqu’ils sont déjà contenus dans l’en-tête.
