Files
pyramid/docs/matrix-sdk/02-rooms.md
T
Bernd Steckmeister 25ed765a03 wip: Sicherungs-Commit aller Änderungen seit April + Arbeitsstruktur (CLAUDE.md, ROADMAP.md, PROGRESS.md, Autopilot)
6 Wochen uncommittete Arbeit (Voice-Channels, LiveKit-Manager, Settings-Modal u.v.m.)
als ein WIP-Commit gesichert, damit nichts verloren geht und der Pi den aktuellen
Stand klonen kann. Thematische Aufarbeitung: siehe ROADMAP M0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrAGBxBT6GfPXzeWQ4AXb
2026-07-03 05:47:18 +02:00

10 KiB

02 — Räume & Spaces


Inhaltsverzeichnis


Raum erstellen

// Einfacher privater Raum
final roomId = await client.createRoom(
  name: 'Mein Raum',
  topic: 'Willkommen!',
  visibility: Visibility.private,      // oder .public
  preset: CreateRoomPreset.privateChat, // oder .publicChat, .trustedPrivateChat
);

// Raum mit Einladungen
final roomId = await client.createRoom(
  name: 'Projekt Alpha',
  invite: ['@bob:example.org', '@charlie:example.org'],
  enableEncryption: true,              // E2EE aktivieren
);

// Raum mit Alias
final roomId = await client.createRoom(
  name: 'Öffentlicher Kanal',
  roomAliasName: 'kanal',             // → #kanal:example.org
  visibility: Visibility.public,
  preset: CreateRoomPreset.publicChat,
);

// Raum mit Custom Power Levels
final roomId = await client.createRoom(
  name: 'Moderiert',
  powerLevelContentOverride: PowerLevelsContent(
    eventsDefault: 50,                 // Nur Moderatoren senden
    usersDefault: 0,
    users: {'@admin:example.org': 100},
  ),
);

// Raum mit History-Visibility
final roomId = await client.createRoom(
  initialState: [
    StateEvent(
      type: EventTypes.HistoryVisibility,
      content: {'history_visibility': 'invited'}, // shared|invited|joined|world_readable
    ),
  ],
);

Raum beitreten

// Per Raum-ID
await client.joinRoom('!roomId:server');

// Per Alias
await client.joinRoomOrAlias('#kanal:example.org');

// Per Invite-Link / Matrix-URI
// matrix:r/kanal:example.org
// https://matrix.to/#/#kanal:example.org
final uri = MatrixUri.parse('https://matrix.to/#/#kanal:example.org');
if (uri != null) {
  await client.joinRoomOrAlias(uri.primaryIdentifier);
}

// Mit Via-Servern (für föderierte Räume)
await client.joinRoom(
  '!roomId:remoteserver.org',
  serverName: ['remoteserver.org', 'matrix.org'],
);

Raum verlassen

// Verlassen (bleibt in Left-State)
final room = client.getRoomById('!roomId:server')!;
await room.leave();

// Vergessen (entfernt aus Raumliste)
await room.forget();

// Verlassen + Vergessen
await room.leave();
await room.forget();

Raumliste

// Alle Räume (joined + invited)
final allRooms = client.rooms;

// Nur beigetretene
final joinedRooms = client.rooms
    .where((r) => r.membership == Membership.join)
    .toList();

// Nur Einladungen
final invites = client.rooms
    .where((r) => r.membership == Membership.invite)
    .toList();

// Sortiert nach letzter Aktivität
final sorted = [...client.rooms]
  ..sort((a, b) => (b.lastEvent?.originServerTs ?? DateTime(0))
      .compareTo(a.lastEvent?.originServerTs ?? DateTime(0)));

// Raum per ID
final room = client.getRoomById('!roomId:server');

// Raum per Alias auflösen
final resolved = await client.getRoomIdByAlias('#alias:server');
// resolved.roomId

// Ungelesene Nachrichten
final unread = client.rooms.fold<int>(
  0, (sum, r) => sum + r.notificationCount);

Raum-Metadaten lesen

final room = client.getRoomById('!roomId:server')!;

// Basis-Info
print(room.id);                          // !roomId:server
print(room.name);                        // Anzeigename (aufgelöst)
print(room.topic);                       // Thema
print(room.avatar);                      // mxc://… URI
print(room.canonicalAlias);             // #alias:server
print(room.membership);                  // Membership.join/invite/leave/ban
print(room.isDirectChat);               // DM?
print(room.directChatMatrixID);         // Gegenüber-MXID bei DM
print(room.isEncrypted);                // E2EE aktiv?
print(room.joinRules);                  // JoinRules.public/invite/knock/restricted
print(room.guestAccess);               // GuestAccess.forbidden/canJoin
print(room.historyVisibility);          // HistoryVisibility.*
print(room.notificationCount);          // Ungelesene Notifications
print(room.highlightCount);             // Mentions
print(room.summary.mJoinedMemberCount); // Mitgliederzahl

// Letztes Event
final last = room.lastEvent;
print(last?.body);
print(last?.originServerTs);

// Display-Name (lokalisiert, wie FluffyChat)
final displayName = room.getLocalizedDisplayname();

Raum-Einstellungen ändern

final room = client.getRoomById('!roomId:server')!;

// Name
await room.setName('Neuer Name');

// Thema
await room.setDescription('Neues Thema');

// Avatar (mxc:// URI nach Upload)
final uri = await client.uploadContent(imageBytes, filename: 'avatar.png');
await room.setAvatar(uri);

// Alias setzen / entfernen
await client.setRoomAlias('#neu:example.org', room.id);
await client.deleteRoomAlias('#alt:example.org');
await room.setCanonicalAlias('#neu:example.org'); // Kanonischen Alias setzen

// Join-Rules
await room.setJoinRules(JoinRules.invite);    // private
await room.setJoinRules(JoinRules.public);    // öffentlich
await room.setJoinRules(JoinRules.knock);     // Beitrittsanfragen
await room.setJoinRules(
  JoinRules.restricted,
  allow: [SpaceRoomJoinRulesAllowInner(
    roomId: '!spaceId:server',
    type: SpaceRoomJoinRulesAllowInnerType.mRoomMembership,
  )],
);

// History-Visibility
await room.setHistoryVisibility(HistoryVisibility.shared);
// .shared | .invited | .joined | .worldReadable

// Guest Access
await room.setGuestAccess(GuestAccess.canJoin);
await room.setGuestAccess(GuestAccess.forbidden);

// E2EE aktivieren (nicht mehr deaktivierbar!)
await room.enableEncryption();

// Generischer State-Event senden
await room.client.sendState(
  room.id,
  'm.room.custom_event',
  {'key': 'value'},
  stateKey: '',
);

Mitglieder

final room = client.getRoomById('!roomId:server')!;

// Alle Mitglieder (gecacht)
final members = room.getParticipants();

// Vollständige Mitgliederliste vom Server laden
final allMembers = await room.requestParticipants();

// Einzelnen User holen
final user = room.unsafeGetUserFromMemoryOrFallback('@alice:server');
print(user.displayName);
print(user.avatarUrl);
print(user.powerLevel);
print(user.membership); // Membership.join/invite/leave/ban/knock

// Aktuell tippende User
final typingUsers = room.typingUsers; // List<User>

// Power Level eines Users
final pl = room.getPowerLevelByUserId('@alice:server');

// Eigener Power Level
final myPl = room.ownPowerLevel;

Einladen, Kick & Ban

final room = client.getRoomById('!roomId:server')!;

// Einladen
await room.invite('@alice:server');

// Kick (entfernt, kann wieder beitreten)
await room.kick('@alice:server', reason: 'Regeln verletzt');

// Ban (dauerhaft gesperrt)
await room.ban('@alice:server', reason: 'Spam');

// Unban
await room.unban('@alice:server');

// Eigene Berechtigungen prüfen
room.canBan;     // bool
room.canKick;    // bool
room.canInvite;  // bool
room.canSendEvent(EventTypes.Message); // bool

Power Levels

final room = client.getRoomById('!roomId:server')!;

// Power Level eines Users setzen
await room.setPower('@alice:server', 50); // 0=User, 50=Mod, 100=Admin

// Power Level Content auslesen
final plContent = room.getState(EventTypes.RoomPowerLevels)
    ?.content
    .tryGet<Map>('users');

// Vollständig setzen
await room.client.sendState(
  room.id,
  EventTypes.RoomPowerLevels,
  {
    'ban': 50,
    'events': {
      'm.room.name': 50,
      'm.room.power_levels': 100,
    },
    'events_default': 0,
    'invite': 50,
    'kick': 50,
    'redact': 50,
    'state_default': 50,
    'users': {
      '@alice:server': 100,
      '@mod:server': 50,
    },
    'users_default': 0,
  },
  stateKey: '',
);

// Berechtigungen prüfen
room.canSendDefaultMessages;       // Nachrichten senden
room.canChangeStateEvent(EventTypes.RoomName); // State-Events ändern

Öffentliches Raumverzeichnis

// Öffentliche Räume suchen
final result = await client.queryPublicRooms(
  limit: 20,
  filter: PublicRoomQueryFilter(genericSearchTerm: 'flutter'),
  server: 'matrix.org',             // optionaler anderer Server
);

for (final room in result.chunk) {
  print(room.name);
  print(room.roomId);
  print(room.numJoinedMembers);
  print(room.worldReadable);
}

// Nächste Seite
final next = await client.queryPublicRooms(
  since: result.nextBatch,
  limit: 20,
);

Spaces

// Space erstellen
final spaceId = await client.createRoom(
  name: 'Mein Space',
  creationContent: {'type': 'm.space'}, // markiert als Space
  visibility: Visibility.private,
);

// Kind-Raum zu Space hinzufügen
final space = client.getRoomById(spaceId)!;
await space.client.sendState(
  spaceId,
  EventTypes.SpaceChild,
  {
    'via': ['example.org'],
    'suggested': true,
    'auto_join': false,
  },
  stateKey: '!childRoomId:server',
);

// Space-Kinder auflisten (vom Server)
final children = await client.getSpaceHierarchy(
  spaceId,
  maxDepth: 2,
  suggestedOnly: false,
);
for (final room in children.rooms) {
  print(room.name);
  print(room.roomType);          // 'm.space' oder null
  print(room.childrenState);     // Sub-Spaces
}

// Kind-Raum entfernen
await space.client.sendState(
  spaceId,
  EventTypes.SpaceChild,
  {},                            // leeres Content → entfernt
  stateKey: '!childRoomId:server',
);

// Space-Parent setzen (vom Kind-Raum aus)
await client.sendState(
  '!childRoomId:server',
  EventTypes.SpaceParent,
  {
    'via': ['example.org'],
    'canonical': true,
  },
  stateKey: spaceId,
);

// Alle Spaces des Users
final spaces = client.rooms.where((r) =>
    r.membership == Membership.join &&
    r.getState('m.room.create')?.content['type'] == 'm.space');

Room Upgrades

// Raum auf neuere Version upgraden (erstellt Nachfolger-Raum)
final newRoomId = await client.upgradeRoom(
  '!alterRaumId:server',
  newVersion: '11',                // Aktuelle empfohlene Version
);
// Alle Mitglieder werden automatisch in den neuen Raum eingeladen
// tombstone-Event wird im alten Raum gesetzt

// Tombstone lesen
final tombstone = room.getState(EventTypes.RoomTombstone);
final replacement = tombstone?.content['replacement_room'] as String?;