Files
Bernd Steckmeister f9979e4f32 fix: beim Gott-Datei-Split verlorene Erklärkommentare wiederhergestellt
Qualitäts-Review-Befund (a): Alle 6 part/part-of-Aufteilungen hatten Code
vollständig übernommen (Zeilen-Multiset-Vergleich Original vs. heutiger
Stand: keine Code-Zeile verloren, keine hinzugefügt), ABER erklärende
Kommentare über den Klassen gingen verloren (24 Blöcke, u. a. die
HTML-Subset-Doku über _MatrixHtmlText, die Selbst-Aussperr-Warnung über
updatePowerLevelsSafely, TURN-Verbrauchskarte, PDF-Cache/Viewer-Doku).
Alle an den richtigen Klassen wiederhergestellt; reine Trennlinien und
ALL-CAPS-Abschnittslabels bewusst nicht (Gott-Datei-Navigation, durch
die neuen Dateinamen ersetzt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:37:48 +02:00

210 lines
6.0 KiB
Dart

part of '../message_group.dart';
class _Avatar extends ConsumerStatefulWidget {
final String initial;
final Color color;
final PyramidTheme pt;
final double size;
final Uri? avatarUri;
const _Avatar({
required this.initial,
required this.color,
required this.pt,
required this.size,
this.avatarUri,
});
@override
ConsumerState<_Avatar> createState() => _AvatarState();
}
class _AvatarState extends ConsumerState<_Avatar> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final client = ref.watch(matrixClientProvider).valueOrNull;
final radius = BorderRadius.circular(widget.size / 2);
Widget child;
if (client != null && widget.avatarUri != null) {
child = MxcAvatar(
mxcUri: widget.avatarUri,
client: client,
size: widget.size,
borderRadius: radius,
placeholder: (_) => _InitialBox(
initial: widget.initial,
color: widget.color,
size: widget.size,
radius: radius,
),
);
} else {
child = _InitialBox(
initial: widget.initial,
color: widget.color,
size: widget.size,
radius: radius,
);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutBack,
transform: _hovered
? Matrix4.diagonal3Values(1.06, 1.06, 1.0)
: Matrix4.identity(),
transformAlignment: Alignment.center,
width: widget.size,
height: widget.size,
child: child,
),
);
}
}
class _InitialBox extends StatelessWidget {
final String initial;
final Color color;
final double size;
final BorderRadius radius;
const _InitialBox({required this.initial, required this.color, required this.size, required this.radius});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(color: color, borderRadius: radius),
child: Center(
child: Text(initial, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
),
);
}
}
// Inline "Neue Nachrichten" divider rendered inside a MessageGroup when the
// fullyRead event is in the middle of the group (same sender, <5 min window).
class _InlineUnreadDivider extends StatelessWidget {
final PyramidTheme pt;
const _InlineUnreadDivider({required this.pt});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 4),
child: Row(
children: [
Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
'Neue Nachrichten',
style: TextStyle(color: pt.accent, fontSize: 11, fontWeight: FontWeight.w600),
),
),
Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)),
],
),
);
}
}
// Date divider between message groups
class DateDivider extends StatelessWidget {
final DateTime date;
const DateDivider({super.key, required this.date});
@override
Widget build(BuildContext context) {
final pt = PyramidTheme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Row(
children: [
Expanded(child: Divider(color: pt.border, height: 1)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
_formatDate(date),
style: TextStyle(
color: pt.fgDim,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 0.06 * 11,
),
),
),
Expanded(child: Divider(color: pt.border, height: 1)),
],
),
);
}
String _formatDate(DateTime d) {
final now = DateTime.now();
if (d.year == now.year && d.month == now.month && d.day == now.day) {
return 'Heute · ${d.day}. ${_monthName(d.month)}';
}
final yesterday = now.subtract(const Duration(days: 1));
if (d.year == yesterday.year && d.month == yesterday.month && d.day == yesterday.day) {
return 'Gestern';
}
return '${d.day}. ${_monthName(d.month)} ${d.year}';
}
String _monthName(int m) => const [
'', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
][m];
}
class _MessageStatus extends StatelessWidget {
final Event lastEvent;
final PyramidTheme pt;
const _MessageStatus({required this.lastEvent, required this.pt});
@override
Widget build(BuildContext context) {
final status = lastEvent.status;
if (status == EventStatus.error) {
return Icon(Icons.error_outline_rounded, size: 12, color: pt.danger);
}
if (status == EventStatus.sending) {
return Icon(Icons.access_time_rounded, size: 12, color: pt.fgDim);
}
// Sent (delivered). "Read" is shown by the per-event ✓✓ marker.
return Icon(Icons.done_rounded, size: 13, color: pt.fgDim);
}
}
// Read receipt: a ✓✓ shown at the partner's last-read message (any sender),
// so it moves next to their own message when they reply.
class _ReadMarker extends StatelessWidget {
final PyramidTheme pt;
const _ReadMarker({required this.pt});
// Goldener Doppelhaken — bewusst nicht die Akzentfarbe, damit "gelesen"
// unabhängig vom Theme sofort erkennbar ist.
static const _gold = Color(0xFFE6B422);
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(right: 24, top: 1, bottom: 1),
child: Align(
alignment: Alignment.centerRight,
child: Icon(Icons.done_all_rounded, size: 13, color: _gold),
),
);
}
}