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:
co-authored by
Claude Opus 5.5
parent
87b472a373
commit
de24053167
@@ -30,6 +30,15 @@ class _ChatHeader extends ConsumerWidget {
|
||||
final client = ref.watch(matrixClientProvider).valueOrNull;
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final isMobile = size.width < 900 || size.height < 500;
|
||||
// Gruppenchat (kein Server-Kanal, kein Sprachkanal): Kopf mit Avatar +
|
||||
// Mitgliederzahl und Gruppenanruf (UI-Audit S3, Bernd 2026-10-09).
|
||||
final isGroup = room != null &&
|
||||
isGroupChatRoom(room!) &&
|
||||
!ref.watch(spaceChannelIdsProvider).contains(room!.id);
|
||||
final groupCallRunning = isGroup &&
|
||||
otherCallParticipants(
|
||||
ref.watch(voiceParticipantsProvider(room!.id)), room!)
|
||||
.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
height: 52,
|
||||
@@ -53,6 +62,8 @@ class _ChatHeader extends ConsumerWidget {
|
||||
// Room icon / avatar
|
||||
if (isDm && client != null) ...[
|
||||
_DmHeaderAvatar(room: room, name: name, client: client),
|
||||
] else if (isGroup && client != null) ...[
|
||||
_GroupHeaderAvatar(room: room!, client: client, pt: pt),
|
||||
] else if (!isDm) ...[
|
||||
Icon(
|
||||
isEncrypted ? Icons.lock_outline_rounded : Icons.tag_rounded,
|
||||
@@ -85,6 +96,28 @@ class _ChatHeader extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (isGroup)
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: pt.fg,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
_groupSubtitle(room!),
|
||||
style: TextStyle(color: pt.fgDim, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
name,
|
||||
@@ -92,7 +125,9 @@ class _ChatHeader extends ConsumerWidget {
|
||||
color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
if (!isDm && topic.isNotEmpty) ...[
|
||||
if (isGroup)
|
||||
const SizedBox.shrink()
|
||||
else if (!isDm && topic.isNotEmpty) ...[
|
||||
Container(width: 1, height: 18, color: pt.border),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -107,18 +142,25 @@ class _ChatHeader extends ConsumerWidget {
|
||||
// Right actions
|
||||
Row(
|
||||
children: [
|
||||
if (isDm) ...[
|
||||
if (isDm || isGroup) ...[
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.phone_rounded),
|
||||
tooltip: 'Anrufen',
|
||||
onPressed: () => _startCall(ref, room, cam: false),
|
||||
icon: Icon(Icons.phone_rounded,
|
||||
color: groupCallRunning ? pt.online : null),
|
||||
tooltip: groupCallRunning
|
||||
? 'Gruppenanruf beitreten'
|
||||
: (isGroup ? 'Gruppenanruf' : 'Anrufen'),
|
||||
onPressed: () => isGroup
|
||||
? startOrJoinGroupCall(context, ref, room!)
|
||||
: _startCall(ref, room, cam: false),
|
||||
),
|
||||
PyrIconBtn(
|
||||
size: 28,
|
||||
icon: const Icon(Icons.videocam_rounded),
|
||||
tooltip: 'Videoanruf',
|
||||
onPressed: () => _startCall(ref, room, cam: true),
|
||||
onPressed: () => isGroup
|
||||
? startOrJoinGroupCall(context, ref, room!, video: true)
|
||||
: _startCall(ref, room, cam: true),
|
||||
),
|
||||
if (size.width > 700)
|
||||
Container(
|
||||
@@ -635,6 +677,43 @@ class _DmExpandingHeaderState extends ConsumerState<_DmExpandingHeader> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Untertitel im Gruppen-Kopf: Mitgliederzahl, dahinter das Thema.
|
||||
String _groupSubtitle(Room room) {
|
||||
final count = room.summary.mJoinedMemberCount ?? room.getParticipants().length;
|
||||
final members = count == 1 ? '1 Mitglied' : '$count Mitglieder';
|
||||
final topic = room.topic.trim();
|
||||
return topic.isEmpty ? members : '$members · $topic';
|
||||
}
|
||||
|
||||
/// Runder Gruppen-Avatar (Raumbild oder Gruppen-Symbol).
|
||||
class _GroupHeaderAvatar extends StatelessWidget {
|
||||
final Room room;
|
||||
final Client client;
|
||||
final PyramidTheme pt;
|
||||
const _GroupHeaderAvatar(
|
||||
{required this.room, required this.client, required this.pt});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const size = 32.0;
|
||||
Widget fallback(BuildContext _) => Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration:
|
||||
BoxDecoration(color: pt.accentSoft, shape: BoxShape.circle),
|
||||
child: Icon(Icons.groups_rounded, size: 18, color: pt.accent),
|
||||
);
|
||||
if (room.avatar == null) return Builder(builder: fallback);
|
||||
return MxcAvatar(
|
||||
mxcUri: room.avatar,
|
||||
client: client,
|
||||
size: size,
|
||||
borderRadius: BorderRadius.circular(size / 2),
|
||||
placeholder: fallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DmHeaderAvatar extends StatelessWidget {
|
||||
final Room? room;
|
||||
final String name;
|
||||
|
||||
@@ -521,6 +521,8 @@ class _ChatViewState extends ConsumerState<ChatView> {
|
||||
),
|
||||
const _ConnectionBanner(),
|
||||
const _ActiveCallBar(),
|
||||
if (room != null && !isInvite)
|
||||
GroupCallBanner(room: room),
|
||||
Expanded(
|
||||
child: isInvite
|
||||
? _InvitePanel(room: room!, pt: pt)
|
||||
|
||||
Reference in New Issue
Block a user