f9979e4f32
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>
210 lines
6.0 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|