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]>
219 lines
6.4 KiB
Dart
219 lines
6.4 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;
|
||
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),
|
||
),
|
||
);
|
||
}
|
||
}
|