我试图让聊天变得更友好,并使其更容易找到聊天频道中的人数。到目前为止,我通过这个 hacky vibe-coded 主题组件(如果有人觉得有用,则采用 CC-0 许可)实现了这一点。
扩展源代码
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();
}
});
结果:

然而,我想知道这是否可以作为默认实现。它比以下方式更容易发现:
- 点击频道标题
- 点击成员选项卡
此外,有了成员计数器,就可以移除设置中的选项卡,因为它们已经包含在标题中了。
