Files
pyramid/lib/features/rooms/panel/rooms_mobile_tile.dart
T
Bernd SteckmeisterandClaude Opus 5.5 de24053167 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]>
2026-10-09 11:58:31 +02:00

175 lines
6.1 KiB
Dart

part of '../rooms_panel.dart';
// Chatlisten-Eintrag im Handy-Layout (Bernds Feedback 2026-10-07: die Handy-App
// soll sich wie ein gewohnter Messenger anfühlen): großer Avatar, Name mit
// Uhrzeit der letzten Nachricht, darunter die letzte Nachricht als Vorschau
// und der Ungelesen-Zähler. Am PC bleibt die kompakte Discord-Liste
// (_RoomItem); Voice-Kanäle behalten ihre eigene Darstellung.
const _kTileAvatar = 46.0;
class _MobileRoomTile extends StatelessWidget {
final Room room;
final bool isActive;
final PyramidTheme pt;
final String? myUserId;
final VoidCallback onTap;
final VoidCallback onLongPress;
const _MobileRoomTile({
required this.room,
required this.isActive,
required this.pt,
required this.myUserId,
required this.onTap,
required this.onLongPress,
});
@override
Widget build(BuildContext context) {
final name = room.getLocalizedDisplayname();
final unread = room.notificationCount;
final mention = room.highlightCount > 0;
final last = room.lastEvent;
final preview = _roomPreview(room, myUserId);
final time = last == null ? null : chatListTime(last.originServerTs);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
onLongPress: onLongPress,
child: Container(
color: isActive ? pt.accentSoft : Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9),
child: Row(
children: [
room.isDirectChat
? _DmAvatar(room: room, pt: pt, size: _kTileAvatar)
: _RoomTileAvatar(room: room, pt: pt),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: pt.fg,
fontSize: 15,
fontWeight:
unread > 0 ? FontWeight.w700 : FontWeight.w600,
),
),
),
if (time != null) ...[
const SizedBox(width: 8),
Text(
time,
style: TextStyle(
color: unread > 0 ? pt.accent : pt.fgDim,
fontSize: 12,
fontWeight:
unread > 0 ? FontWeight.w600 : FontWeight.w400,
),
),
],
],
),
const SizedBox(height: 3),
Row(
children: [
Expanded(
child: Text(
preview ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread > 0 ? pt.fgMuted : pt.fgDim,
fontSize: 13.5,
),
),
),
// Immer im Baum, damit der Zähler beim Erscheinen und
// Hochzählen federnd aufploppen kann.
SpringBump(
value: unread,
child: unread > 0
? Padding(
padding: const EdgeInsets.only(left: 8),
child: _UnreadBadge(
count: unread, isMention: mention, pt: pt),
)
: const SizedBox.shrink(),
),
],
),
],
),
),
],
),
),
);
}
}
/// Avatar für Räume (kein DM): Raumbild oder Symbol. Gruppen (kein
/// Server-Kanal) sind rund wie Personen, Server-Kanäle abgerundet mit „#“.
class _RoomTileAvatar extends ConsumerWidget {
final Room room;
final PyramidTheme pt;
final double size;
const _RoomTileAvatar(
{required this.room, required this.pt, this.size = _kTileAvatar});
@override
Widget build(BuildContext context, WidgetRef ref) {
final client = ref.watch(matrixClientProvider).valueOrNull;
final isChannel = ref.watch(spaceChannelIdsProvider).contains(room.id);
final radius = BorderRadius.circular(isChannel ? size * 0.3 : size / 2);
final fallback = Container(
width: size,
height: size,
decoration: BoxDecoration(
color: isChannel ? pt.bg3 : pt.accentSoft,
borderRadius: radius,
),
child: Icon(
isChannel
? (room.encrypted ? Icons.lock_outline_rounded : Icons.tag_rounded)
: Icons.groups_rounded,
size: size * 0.5,
color: isChannel ? pt.fgMuted : pt.accent,
),
);
if (client == null || room.avatar == null) return fallback;
return MxcAvatar(
mxcUri: room.avatar,
client: client,
size: size,
borderRadius: radius,
placeholder: (_) => fallback,
);
}
}
/// Vorschau der letzten Nachricht: gleiche Texte wie in Benachrichtigungen
/// (Medien als „📷 Foto“ usw., Antwort-Zitat entfernt), in Gruppen mit
/// Absender, eigene Nachrichten mit „Du:“.
String? _roomPreview(Room room, String? myUserId) {
final e = room.lastEvent;
if (e == null) return null;
if (e.type == EventTypes.Encrypted) return '🔒 Verschlüsselte Nachricht';
final body = notificationBody(e);
if (body == null) return null;
if (e.senderId == myUserId) return 'Du: $body';
if (room.isDirectChat) return body;
return '${e.senderFromMemoryOrFallback.calcDisplayname()}: $body';
}