part of '../rooms_panel.dart'; class _DmAvatar extends ConsumerStatefulWidget { final Room room; final PyramidTheme pt; const _DmAvatar({required this.room, required this.pt}); @override ConsumerState<_DmAvatar> createState() => _DmAvatarState(); } class _DmAvatarState extends ConsumerState<_DmAvatar> { CachedPresence? _presence; StreamSubscription? _sub; @override void initState() { super.initState(); _loadPresence(); } @override void dispose() { _sub?.cancel(); super.dispose(); } Future _loadPresence() async { final client = ref.read(matrixClientProvider).valueOrNull; if (client == null) return; final userId = widget.room.directChatMatrixID; if (userId == null) return; try { final p = await client.fetchCurrentPresence(userId); if (mounted) setState(() => _presence = p); } catch (_) {} _sub = client.onPresenceChanged.stream .where((p) => p.userid == userId) .listen((p) { if (mounted) setState(() => _presence = p); }); } Color? _dotColor() { final p = _presence; if (p == null) return null; // Server-Presence kann veraltet sein (z.B. nie als offline gemeldet). // Nur als online werten, wenn die letzte Aktivität wirklich frisch ist. final last = p.lastActiveTimestamp; final fresh = last != null && DateTime.now().difference(last) < const Duration(minutes: 5); if (p.presence == PresenceType.online && (p.currentlyActive == true || fresh)) { return PyramidColors.online; } if (p.presence == PresenceType.unavailable) return PyramidColors.away; return null; } @override Widget build(BuildContext context) { final name = widget.room.getLocalizedDisplayname(); final initial = name.isNotEmpty ? name[0].toUpperCase() : '?'; final color = _colorFromName(name); final client = ref.watch(matrixClientProvider).valueOrNull; Widget avatar; if (client != null && widget.room.avatar != null) { avatar = MxcAvatar( mxcUri: widget.room.avatar, client: client, size: 22, borderRadius: BorderRadius.circular(11), placeholder: (_) => _InitialCircle(initial: initial, color: color, size: 22), ); } else { avatar = _InitialCircle(initial: initial, color: color, size: 22); } final dot = _dotColor(); if (dot == null) return avatar; return Stack(clipBehavior: Clip.none, children: [ avatar, Positioned( right: -2, bottom: -2, child: Container( width: 9, height: 9, decoration: BoxDecoration( color: dot, shape: BoxShape.circle, border: Border.all(color: widget.pt.bg1, width: 1.5), ), ), ), ]); } Color _colorFromName(String name) { const colors = [ Color(0xFF06B6D4), Color(0xFFA855F7), Color(0xFF10B981), Color(0xFFF43F5E), Color(0xFF3B82F6), Color(0xFFF59E0B), ]; if (name.isEmpty) return colors[0]; return colors[name.codeUnitAt(0) % colors.length]; } } class _InitialCircle extends StatelessWidget { final String initial; final Color color; final double size; const _InitialCircle( {required this.initial, required this.color, required this.size}); @override Widget build(BuildContext context) { return Container( width: size, height: size, decoration: BoxDecoration(color: color, shape: BoxShape.circle), child: Center( child: Text(initial, style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.w600))), ); } } class _RoomMenuBtn extends StatefulWidget { final PyramidTheme pt; final VoidCallback onInvite; final VoidCallback onSettings; final VoidCallback onLeave; const _RoomMenuBtn({required this.pt, required this.onInvite, required this.onSettings, required this.onLeave}); @override State<_RoomMenuBtn> createState() => _RoomMenuBtnState(); } class _RoomMenuBtnState extends State<_RoomMenuBtn> { bool _hovered = false; @override Widget build(BuildContext context) { return PopupMenuButton( color: widget.pt.bg2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: widget.pt.border)), offset: const Offset(-100, 24), tooltip: '', onSelected: (v) { if (v == 'invite') widget.onInvite(); if (v == 'settings') widget.onSettings(); if (v == 'leave') widget.onLeave(); }, itemBuilder: (_) => [ PopupMenuItem( value: 'invite', height: 36, child: Row(children: [ Icon(Icons.person_add_outlined, size: 14, color: widget.pt.fgMuted), const SizedBox(width: 8), Text('Nutzer einladen', style: TextStyle(color: widget.pt.fg, fontSize: 13)), ]), ), PopupMenuItem( value: 'settings', height: 36, child: Row(children: [ Icon(Icons.settings_rounded, size: 14, color: widget.pt.fgMuted), const SizedBox(width: 8), Text('Einstellungen', style: TextStyle(color: widget.pt.fg, fontSize: 13)), ]), ), PopupMenuDivider(height: 1), PopupMenuItem( value: 'leave', height: 36, child: Row(children: [ Icon(Icons.exit_to_app_rounded, size: 14, color: widget.pt.danger), const SizedBox(width: 8), Text('Raum verlassen', style: TextStyle(color: widget.pt.danger, fontSize: 13)), ]), ), ], child: MouseRegion( onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 120), width: 20, height: 20, decoration: BoxDecoration( color: _hovered ? widget.pt.bgActive : Colors.transparent, borderRadius: BorderRadius.circular(4), ), child: Center( child: Icon(Icons.more_horiz_rounded, size: 12, color: _hovered ? widget.pt.fg : widget.pt.fgDim)), ), ), ); } } class _HoverAction extends StatefulWidget { final IconData icon; final PyramidTheme pt; final VoidCallback? onTap; const _HoverAction({required this.icon, required this.pt, this.onTap}); @override State<_HoverAction> createState() => _HoverActionState(); } class _HoverActionState extends State<_HoverAction> { bool _hovered = false; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.onTap, child: MouseRegion( cursor: widget.onTap != null ? SystemMouseCursors.click : MouseCursor.defer, onEnter: (_) => setState(() => _hovered = true), onExit: (_) => setState(() => _hovered = false), child: AnimatedContainer( duration: const Duration(milliseconds: 120), width: 20, height: 20, decoration: BoxDecoration( color: _hovered ? widget.pt.bgActive : Colors.transparent, borderRadius: BorderRadius.circular(4), ), child: Center( child: Icon(widget.icon, size: 12, color: _hovered ? widget.pt.fg : widget.pt.fgDim)), ), ), ); } } class _UnreadBadge extends StatelessWidget { final int count; final bool isMention; final PyramidTheme pt; const _UnreadBadge( {required this.count, required this.isMention, required this.pt}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 5), constraints: const BoxConstraints(minWidth: 18, minHeight: 18), decoration: BoxDecoration( color: isMention ? pt.danger : pt.accent, borderRadius: BorderRadius.circular(999), ), child: Center( child: Text(count > 99 ? '99+' : '$count', style: TextStyle( color: isMention ? Colors.white : pt.accentFg, fontSize: 10, fontWeight: FontWeight.w700))), ); } }