diff --git a/internal/server/members.go b/internal/server/members.go index 06b0db8..83a3abe 100644 --- a/internal/server/members.go +++ b/internal/server/members.go @@ -35,6 +35,8 @@ type memberResponse struct { Avatar string `json:"avatar"` Status string `json:"status"` StatusText string `json:"status_text"` + IsBot bool `json:"is_bot,omitempty"` + BotType string `json:"bot_type,omitempty"` } func (h *MemberHandler) ListMembers(w http.ResponseWriter, r *http.Request) { @@ -82,6 +84,28 @@ func (h *MemberHandler) ListMembers(w http.ResponseWriter, r *http.Request) { members = append(members, m) } + // Bots added to this server (separate BOTS section in the member list). + botRows, err := h.db.QueryContext(r.Context(), ` + SELECT b.id, b.name, COALESCE(b.avatar, ''), COALESCE(b.bot_type, '') + FROM bot_servers bs + JOIN bots b ON b.id = bs.bot_id + WHERE bs.server_id = $1 + ORDER BY b.name + `, serverID) + if err == nil { + defer botRows.Close() + for botRows.Next() { + var m memberResponse + if err := botRows.Scan(&m.ID, &m.Username, &m.Avatar, &m.BotType); err != nil { + continue + } + m.DisplayName = m.Username + m.Status = "online" // ponytail: no bot presence yet; always show online + m.IsBot = true + members = append(members, m) + } + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(members) } diff --git a/web/src/components/ChatArea.tsx b/web/src/components/ChatArea.tsx index 4b70e50..bd672ca 100644 --- a/web/src/components/ChatArea.tsx +++ b/web/src/components/ChatArea.tsx @@ -326,7 +326,9 @@ export function ChatArea() { const channels = activeServerId ? channelsByServer[activeServerId] || [] : []; const activeChannel = channels.find((c) => c.id === activeChannelId); const members = activeServerId ? membersByServer[activeServerId] || [] : []; - const memberUsernames = useMemo(() => new Set(members.map((m) => m.username)), [members]); + // Humans only for mentions / nickname lookup (bots live in member list separately). + const humanMembers = useMemo(() => members.filter((m) => !m.is_bot), [members]); + const memberUsernames = useMemo(() => new Set(humanMembers.map((m) => m.username)), [humanMembers]); const markRead = useReadStatesStore((s) => s.markRead); const readStates = useReadStatesStore((s) => s.states); @@ -553,7 +555,7 @@ export function ChatArea() { if (!isDropdownOpen) return; const itemCount = mq !== null - ? members.filter((m) => + ? humanMembers.filter((m) => m.username.toLowerCase().includes(mq.toLowerCase()) || m.display_name?.toLowerCase().includes(mq.toLowerCase()) ).slice(0, 6).length @@ -574,7 +576,7 @@ export function ChatArea() { e.stopPropagation(); if (mq !== null) { const q = mq.toLowerCase(); - const filtered = members.filter((m) => + const filtered = humanMembers.filter((m) => m.username.toLowerCase().includes(q) || m.display_name?.toLowerCase().includes(q) ).slice(0, 6); @@ -618,7 +620,7 @@ export function ChatArea() { }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [members, currentUser, activeChannelId, sendMessage, replyToMessage, handleMentionSelect]); + }, [humanMembers, currentUser, activeChannelId, sendMessage, replyToMessage, handleMentionSelect]); const handleSubmit = useCallback(async () => { if (mentionQuery !== null || commandQuery !== null) return; @@ -902,7 +904,7 @@ export function ChatArea() { )}