part of '../chat_view.dart'; class _TypingIndicator extends StatefulWidget { final Room room; final PyramidTheme pt; const _TypingIndicator({required this.room, required this.pt}); @override State<_TypingIndicator> createState() => _TypingIndicatorState(); } class _TypingIndicatorState extends State<_TypingIndicator> { StreamSubscription? _sub; List _typing = []; StreamSubscription _listenTyping(Room room) { // client.onSync fires after every sync — cheap to filter for typing events. return room.client.onSync.stream.listen((_) { if (!mounted) return; final typing = room.typingUsers .where((u) => u.id != room.client.userID) .toList(); if (typing.length != _typing.length || !typing.every((u) => _typing.any((t) => t.id == u.id))) { setState(() => _typing = typing); } }); } @override void initState() { super.initState(); _sub = _listenTyping(widget.room); } @override void didUpdateWidget(_TypingIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.room.id != widget.room.id) { _sub?.cancel(); setState(() => _typing = []); _sub = _listenTyping(widget.room); } } @override void dispose() { _sub?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { if (_typing.isEmpty) return const SizedBox.shrink(); final pt = widget.pt; final names = _typing.map((u) => u.calcDisplayname()).toList(); final label = names.length == 1 ? '${names[0]} tippt…' : names.length == 2 ? '${names[0]} und ${names[1]} tippen…' : '${names[0]} und ${names.length - 1} weitere tippen…'; return Padding( padding: const EdgeInsets.fromLTRB(20, 2, 20, 0), child: Row( children: [ _TypingDots(color: pt.fgDim), const SizedBox(width: 8), Flexible( child: Text( label, style: TextStyle(color: pt.fgDim, fontSize: 12, fontStyle: FontStyle.italic), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ); } } class _TypingDots extends StatefulWidget { final Color color; const _TypingDots({required this.color}); @override State<_TypingDots> createState() => _TypingDotsState(); } class _TypingDotsState extends State<_TypingDots> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), )..repeat(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctrl, builder: (context, child) { return Row( mainAxisSize: MainAxisSize.min, children: List.generate(3, (i) { // Each dot peaks at a different phase. final phase = ((_ctrl.value * 3) - i).clamp(0.0, 1.0); final opacity = (phase < 0.5 ? phase * 2 : (1 - phase) * 2).clamp(0.3, 1.0); return Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: Opacity( opacity: opacity, child: Container( width: 4, height: 4, decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle), ), ), ); }), ); }, ); } }