25ed765a03
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
437 lines
10 KiB
Markdown
437 lines
10 KiB
Markdown
# 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?;
|
|
```
|