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:
@@ -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?;
|
||||
```
|
||||
Reference in New Issue
Block a user