feat(ui): BOTS section in member list

Members API appends server bots (is_bot). Sidebar groups ONLINE / OFFLINE / BOTS.
Bots get green BOT badge, no kick menu or profile. Mentions and DMs skip bots.
This commit is contained in:
2026-07-15 20:37:05 -04:00
parent 7bf1eaf845
commit fd7fa4a147
5 changed files with 63 additions and 16 deletions
+29 -11
View File
@@ -57,7 +57,7 @@ interface MemberRowProps {
function MemberRow({ member, onProfileClick }: MemberRowProps) {
const presence = usePresenceStore((s) => s.presences[member.id]);
const status = presence?.status ?? member.status;
const status = member.is_bot ? "online" : (presence?.status ?? member.status);
const [menuOpen, setMenuOpen] = useState(false);
const currentUser = useAuthStore((s) => s.user);
const activeServerId = useServerStore((s) => s.activeServerId);
@@ -72,14 +72,21 @@ function MemberRow({ member, onProfileClick }: MemberRowProps) {
return (
<div className="relative">
<div
onClick={() => onProfileClick(member.id)}
className="w-full flex items-center gap-2 px-2 py-1 text-left hover:bg-gb-bg-t cursor-pointer group"
onClick={() => {
if (!member.is_bot) onProfileClick(member.id);
}}
className={`w-full flex items-center gap-2 px-2 py-1 text-left group ${
member.is_bot ? "cursor-default" : "hover:bg-gb-bg-t cursor-pointer"
}`}
>
<span className={statusColor(status)}>{statusIcon(status)}</span>
<span className={`truncate ${usernameColor(status)} flex-1`}>
<span className={`truncate ${member.is_bot ? "text-gb-green" : usernameColor(status)} flex-1`}>
{member.nickname || member.display_name || member.username}
</span>
{!isSelf && (
{member.is_bot && (
<span className="text-gb-bg bg-gb-green px-0.5 font-mono text-[10px] shrink-0">BOT</span>
)}
{!isSelf && !member.is_bot && (
<button
onClick={(e) => {
e.stopPropagation();
@@ -92,7 +99,7 @@ function MemberRow({ member, onProfileClick }: MemberRowProps) {
</button>
)}
</div>
{menuOpen && !isSelf && activeServerId && (
{menuOpen && !isSelf && !member.is_bot && activeServerId && (
<MemberContextMenu
memberId={member.id}
username={member.username}
@@ -155,11 +162,13 @@ export function MemberList() {
}
}, [activeServerId, fetchMembers]);
const online = members.filter((m) => {
const humans = members.filter((m) => !m.is_bot);
const bots = members.filter((m) => m.is_bot);
const online = humans.filter((m) => {
const status = presences[m.id]?.status ?? m.status;
return status !== "offline";
});
const offline = members.filter((m) => {
const offline = humans.filter((m) => {
const status = presences[m.id]?.status ?? m.status;
return status === "offline";
});
@@ -178,7 +187,7 @@ export function MemberList() {
)}
{online.length > 0 && (
<div className="mb-3">
<div className="text-gb-fg-t text-xs uppercase mb-1">ONLINE</div>
<div className="text-gb-fg-t text-xs uppercase mb-1">ONLINE {online.length}</div>
<div className="text-gb-fg-f text-xs mb-1">---</div>
{online.map((m) => (
<MemberRow key={m.id} member={m} onProfileClick={setProfileUserId} />
@@ -186,14 +195,23 @@ export function MemberList() {
</div>
)}
{offline.length > 0 && (
<div>
<div className="text-gb-fg-t text-xs uppercase mb-1">OFFLINE</div>
<div className="mb-3">
<div className="text-gb-fg-t text-xs uppercase mb-1">OFFLINE {offline.length}</div>
<div className="text-gb-fg-f text-xs mb-1">---</div>
{offline.map((m) => (
<MemberRow key={m.id} member={m} onProfileClick={setProfileUserId} />
))}
</div>
)}
{bots.length > 0 && (
<div>
<div className="text-gb-fg-t text-xs uppercase mb-1">BOTS {bots.length}</div>
<div className="text-gb-fg-f text-xs mb-1">---</div>
{bots.map((m) => (
<MemberRow key={m.id} member={m} onProfileClick={setProfileUserId} />
))}
</div>
)}
</div>
{profileUserId && (
<UserProfileModal userId={profileUserId} onClose={() => setProfileUserId(null)} />