Files
Bernd Steckmeister 31e4001fb0 refactor: Gott-Datei chat_view.dart in 6 Dateien aufgeteilt
Fünfter und letzter Durchgang der part/part-of-Aufteilung - reine
Verschiebung, keine Logikänderung. chat_view.dart schrumpft von 2116 auf
33 Zeilen (nur noch Bibliothekskopf). Verifiziert per automatisiertem
Blockvergleich gegen das Original und flutter analyze.

Damit sind alle 5 in der M2-Ist-Analyse gefundenen Gott-Dateien aufgeteilt;
ROADMAP-Punkt "Toten Code & Duplikate entfernen, Ordnerstruktur
vereinheitlichen" abgehakt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 11:03:31 +02:00

137 lines
3.6 KiB
Dart

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<User> _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),
),
),
);
}),
);
},
);
}
}