feat(ui): gemeinsame Chatliste, Gruppenanrufe mit Beitreten-Karte, Bubble-Anrufansicht

- Chats: Direktnachrichten + Gruppen ohne Server zusammen, Filter-Chips
  (Alle/Ungelesen/Personen/Gruppen), Leerzustaende mit Handlung; virtuelles
  'Raeume'-Symbol entfaellt, Server-Kanaele nur noch im Server
- Gruppen: runder Gruppen-Avatar, Kopf mit Mitgliederzahl, Anruf-/Video-Knopf
- Gruppenanruf: LiveKit + Anwesenheit, Start-Nachricht (Push), Karte
  'Gruppenanruf laeuft · Beitreten', Hinweis-Karte im Verlauf
- Anrufansicht: aneinandergedrueckte Bubbles (Federphysik, Sprecher waechst),
  Kacheln umschaltbar, Bildschirm automatisch gross, Gespraechsdauer,
  Matrix-Namen/-Avatare statt LiveKit-IDs
- Tests: test/bubble_physics_test.dart

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-09 11:58:31 +02:00
co-authored by Claude Opus 5.5
parent 87b472a373
commit de24053167
24 changed files with 1550 additions and 314 deletions
+28
View File
@@ -6,3 +6,31 @@ const voiceChannelRoomType = 'io.pyramid.voice_channel';
/// Ist [room] ein Sprachkanal (statt eines Text-Chats)?
bool isVoiceChannelRoom(Room room) =>
room.getState(EventTypes.RoomCreate)?.content['type'] == voiceChannelRoomType;
/// IDs aller Räume, die Kanal eines beigetretenen Servers (Space) sind –
/// auch über Unter-Spaces. Diese Räume stehen nur in ihrem Server, nicht
/// zusätzlich in „Chats“ (Bernd 2026-10-09: „Alles in Chats“).
Set<String> spaceChannelIds(Client client) {
final ids = <String>{};
for (final space in client.rooms) {
if (!space.isSpace || space.membership != Membership.join) continue;
for (final child in space.spaceChildren) {
final id = child.roomId;
if (id != null) ids.add(id);
}
}
return ids;
}
/// Gehört [room] in die gemeinsame Chatliste? Direktnachrichten und Gruppen,
/// die zu keinem beigetretenen Server gehören. [channelIds] stammt aus
/// [spaceChannelIds] (einmal pro Liste berechnen, nicht pro Raum).
bool isChatListRoom(Room room, Set<String> channelIds) {
if (room.isSpace) return false;
if (room.isDirectChat) return true;
return !channelIds.contains(room.id);
}
/// Gruppenchat = kein Space, keine Direktnachricht, kein Sprachkanal.
bool isGroupChatRoom(Room room) =>
!room.isSpace && !room.isDirectChat && !isVoiceChannelRoom(room);