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
This commit is contained in:
Bernd Steckmeister
2026-07-03 05:47:18 +02:00
parent d706ace35f
commit 25ed765a03
195 changed files with 47784 additions and 1236 deletions
+436
View File
@@ -0,0 +1,436 @@
# 02 — Räume & Spaces
---
## Inhaltsverzeichnis
- [Raum erstellen](#raum-erstellen)
- [Raum beitreten](#raum-beitreten)
- [Raum verlassen](#raum-verlassen)
- [Raumliste](#raumliste)
- [Raum-Metadaten lesen](#raum-metadaten-lesen)
- [Raum-Einstellungen ändern](#raum-einstellungen-ändern)
- [Mitglieder](#mitglieder)
- [Einladen & Kick & Ban](#einladen--kick--ban)
- [Öffentliches Raumverzeichnis](#öffentliches-raumverzeichnis)
- [Spaces](#spaces)
- [Room Upgrades](#room-upgrades)
---
## Raum erstellen
```dart
// 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
```dart
// 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
```dart
// 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
```dart
// 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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
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
```dart
// Ö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
```dart
// 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
```dart
// 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?;
```