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'; } /// Sprachkanal im Handy-Layout: Kachel wie ein Chat, darunter wer gerade drin /// ist (Avatare + Namen) bzw. „Antippen zum Beitreten“. class _MobileVoiceTile extends StatelessWidget { final Room room; final bool inVoice; final List> participants; final Client? client; final PyramidTheme pt; final VoidCallback onTap; final VoidCallback onLongPress; const _MobileVoiceTile({ required this.room, required this.inVoice, required this.participants, required this.client, required this.pt, required this.onTap, required this.onLongPress, }); @override Widget build(BuildContext context) { final green = pt.online; final names = participants .map((p) => (p['displayName'] as String? ?? '').split(' ').first) .where((n) => n.isNotEmpty) .toList(); final subtitle = inVoice ? 'Du bist drin · antippen zum Verlassen' : participants.isEmpty ? 'Sprachkanal · antippen zum Beitreten' : names.length <= 2 ? '${names.join(' und ')} ${names.length == 1 ? 'ist' : 'sind'} drin' : '${names.take(2).join(', ')} und ${names.length - 2} weitere'; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, onLongPress: onLongPress, child: Container( color: inVoice ? green.withValues(alpha: 0.10) : Colors.transparent, padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), child: Row( children: [ Container( width: _kTileAvatar, height: _kTileAvatar, decoration: BoxDecoration( color: green.withValues(alpha: inVoice ? 0.30 : 0.14), borderRadius: BorderRadius.circular(_kTileAvatar * 0.3), ), child: Icon( participants.isEmpty ? Icons.volume_up_rounded : Icons.graphic_eq_rounded, size: 22, color: green, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(room.getLocalizedDisplayname(), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600)), const SizedBox(height: 3), Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: participants.isEmpty && !inVoice ? pt.fgDim : green, fontSize: 13.5)), ], ), ), if (participants.isNotEmpty && client != null) ...[ const SizedBox(width: 8), CallAvatarStack( participants: participants, client: client!, ringColor: pt.bg1, size: 26, ), ], ], ), ), ); } }