feat: right-click context menus for channels, groups, and servers

ContextMenu: reusable hook-based component that positions a menu at
the cursor and auto-closes on outside click or Escape.

ChannelList:
- Right-click channel -> Edit Name, Permissions, Delete Channel
- Right-click group header -> Edit Name, Create Channel Here, Delete Group
- Inline rename: replacing name text with an input, commits on Enter/blur
- CreateChannelModal now accepts defaultGroupId to pre-select a group

ServerBar:
- Right-click server icon -> Server Settings, Invite People, Leave Server
- Leave calls DELETE /servers/{id}/members/me

Backend:
- Added LeaveServer handler (DELETE /servers/{serverID}/members/me)
- Server owner cannot leave; must transfer or delete
This commit is contained in:
2026-07-02 08:18:12 -04:00
parent ecb26cb731
commit 31cb295a24
6 changed files with 335 additions and 97 deletions
+36
View File
@@ -24,6 +24,7 @@ func (h *Handler) RegisterRoutes(r chi.Router) {
r.Get("/{serverID}", h.Get)
r.Patch("/{serverID}", h.Update)
r.Delete("/{serverID}", h.Delete)
r.Delete("/{serverID}/members/me", h.LeaveServer)
}
type createServerRequest struct {
@@ -315,3 +316,38 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) LeaveServer(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
serverID := chi.URLParam(r, "serverID")
// Cannot leave a server you own
var ownerID string
err := h.db.QueryRowContext(r.Context(), `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID)
if err != nil {
http.Error(w, `{"error":"server not found"}`, http.StatusNotFound)
return
}
if ownerID == userID {
http.Error(w, `{"error":"server owner cannot leave; transfer ownership or delete the server"}`, http.StatusForbidden)
return
}
result, err := h.db.ExecContext(r.Context(), `DELETE FROM members WHERE user_id = $1 AND server_id = $2`, userID, serverID)
if err != nil {
http.Error(w, `{"error":"failed to leave server"}`, http.StatusInternalServerError)
return
}
rows, _ := result.RowsAffected()
if rows == 0 {
http.Error(w, `{"error":"not a member"}`, http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
}