Mobile: spaces; /hierarchy always sends children_state

Mobile:
- The room list's title picks a space: All chats, or one space, which
  narrows the list to its rooms (and its subspaces' rooms).
- A space browser over /hierarchy: the rooms of a space and of its
  subspaces, joined or joinable, with Join; for those who may, adding an
  existing room or a new one to the space.
- New space in the new-chat sheet; invites to spaces show with the other
  invites and open the space when joined.
- Fixes several setState calls that were handed a Future (reloading the
  members, sessions, push distributor, threads), which Flutter rejects.

Server: /hierarchy left out children_state for a room without children.
The spec requires it on every room (an empty list), and SpaceRoom's
omitempty dropped empty lists too, despite its comment. The famedly Dart
SDK fails to parse such a response. The client API now wraps each room so
the field is always written; the federation response, which wants it left
out of the children it lists, is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-18 23:14:40 +02:00
1 parent 4271d2ab17
commit 879e22141b
10 files changed
+628 -33

No files matched your search

+19 -5
View File
@@ -13,8 +13,18 @@ import (
// hierarchyResponse is what GET /hierarchy returns.
type hierarchyResponse struct {
Rooms []roomserver.SpaceRoom `json:"rooms"`
NextBatch string `json:"next_batch,omitempty"`
Rooms []hierarchyRoom `json:"rooms"`
NextBatch string `json:"next_batch,omitempty"`
}
// hierarchyRoom is a room as /hierarchy shows it to a client:
// children_state is required there, an empty list for a room with no
// children. SpaceRoom leaves an empty one out, as the federation response
// wants for the children it lists; this field, being shallower, takes the
// JSON name over the embedded one.
type hierarchyRoom struct {
roomserver.SpaceRoom
ChildrenState []roomserver.SpaceChildEvent `json:"children_state"`
}
// hierarchyTokenPrefix marks a /hierarchy token: how far into the walk
@@ -62,9 +72,13 @@ func (c *ClientAPI) handleHierarchy(w http.ResponseWriter, r *http.Request, auth
if err != nil {
return roomError(err)
}
resp := hierarchyResponse{Rooms: page.Rooms}
if resp.Rooms == nil {
resp.Rooms = []roomserver.SpaceRoom{}
resp := hierarchyResponse{Rooms: make([]hierarchyRoom, 0, len(page.Rooms))}
for _, room := range page.Rooms {
children := room.ChildrenState
if children == nil {
children = []roomserver.SpaceChildEvent{}
}
resp.Rooms = append(resp.Rooms, hierarchyRoom{SpaceRoom: room, ChildrenState: children})
}
if page.Next != 0 {
resp.NextBatch = hierarchyTokenPrefix + strconv.Itoa(page.Next)
+29
View File
@@ -180,3 +180,32 @@ func TestRoomSummary(t *testing.T) {
t.Errorf("alice's summary of her own room says membership %v", jsonBody(t, rec)["membership"])
}
}
// children_state is required on every room /hierarchy returns, an empty
// list for one with no children: clients that parse the response strictly
// (the famedly Dart SDK) fail on a room without it.
func TestSpaceHierarchyEmptyChildrenState(t *testing.T) {
ts := newTestServer(t, nil)
alice := ts.register("alice", "correct-horse-battery-staple")
space := ts.createSpace(alice, "Empty space", false)
room := ts.createRoom(alice, map[string]any{"name": "Leaf"})
ts.addChild(alice, space, room, nil)
rec := ts.get("/_matrix/client/v1/rooms/"+url.PathEscape(space)+"/hierarchy", alice.AccessToken)
expectStatus(t, rec, http.StatusOK)
raw := decode[struct {
Rooms []map[string]any `json:"rooms"`
}](t, rec)
if len(raw.Rooms) != 2 {
t.Fatalf("got %d rooms, want the space and its child", len(raw.Rooms))
}
for _, r := range raw.Rooms {
children, ok := r["children_state"].([]any)
if !ok {
t.Errorf("%v: children_state = %#v, want a list", r["room_id"], r["children_state"])
}
if r["room_id"] == room && len(children) != 0 {
t.Errorf("the leaf room has children_state %v", children)
}
}
}