Files
pyramid/lib/features/chat/message/message_shared.dart
T
Bernd SteckmeisterandClaude Opus 5.5 8074928505 fix(calls): Bildschirm teilen sichtbar, Verbindung trotz SDK-Wettlauf, Sprech-Ring; Trenner in Gruppen
Bernds Test 2026-10-08 (Streamen ging weder am PC noch am Handy, Ton kam
nicht an, Trenner blieb manchmal stehen) – mit neuem Live-Anruftest
(zwei Testkonten im selben Prozess) nachgestellt und behoben:

- Bildschirm teilen: Die Stromzuordnung rief ein SDK-getFeeds() auf, das es
  nicht gibt; der Fehler wurde geschluckt → Übertragungen wurden nie
  angezeigt und beim Sender nie als aktiv erkannt. Jetzt typisierte
  Zugriffe (remote/localScreenSharingStream, 'stopped' beachtet).
- Verbindungsaufbau: matrix-SDK verwirft Kandidaten, die vor der fertig
  verarbeiteten Einladung bzw. während des Annehmens eintreffen; unter
  Windows wirft getRemoteDescription zudem statt null zu liefern. Pyramid
  puffert alle Kandidaten der Gegenseite und reicht sie direkt an die
  Verbindung nach. Test: vorher nie verbunden, jetzt ~0,5 s (angerufen)
  bzw. ~2,7 s (anrufend), Ton beide Richtungen, Teilen beide Richtungen.
- SDK-Aufrufe in eigener Zone: harmlose Plugin-Fehler (Lautsprecher unter
  Windows) werden protokolliert statt durchzuschlagen; Ergebnisse/Fehler
  per Completer über die Zonengrenze (sonst ewiges Warten).
- Sprech-Ring auch bei 1:1-Anrufen (Pegel aus WebRTC-Statistiken) – zeigt,
  ob das eigene Mikrofon Ton liefert und ob die Gegenseite ankommt; großes
  Bild springt nicht mehr bei jedem Sprechwechsel.
- „Neue Nachrichten“ mitten in einer Gruppe (mehrere Nachrichten desselben
  Absenders) blieb bis zum erneuten Öffnen stehen: Inline-Trenner jetzt nur
  mit Ungelesenem, blendet aus und trägt den Sprungschlüssel. Live-Test.
- „Anruf läuft“-Leiste nicht schon beim Klingeln.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 11:05:46 +02:00

219 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
final bool fading;
const _InlineUnreadDivider({super.key, required this.pt, this.fading = false});
@override
Widget build(BuildContext context) {
// Blendet aus wie der Gruppen-Trenner (vorher blieb dieser „Neue
// Nachrichten“-Trenner mitten in einer Gruppe bis zum erneuten Öffnen
// stehen – Bernd 2026-10-08).
return AnimatedOpacity(
opacity: fading ? 0.0 : 1.0,
duration: const Duration(milliseconds: 700),
curve: Curves.easeOut,
child: 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),
),
);
}
}