fix(ui): UX-Audit – Sackgassen, tote Knöpfe, englische Reste; Screenshot-Tour

Bernds Wunsch 2026-10-08: UI grundsätzlich prüfen (Navigation, Sackgassen,
Anruf-Logik). Audit + Konzept in docs/UI_AUDIT.md, belegt mit neuer
Screenshot-Tour integration_test/ui_audit_tour_test.dart (35 Zustände,
Handy + PC, zwei Testkonten, Test-Space mit Sprachkanal).

Sofort behoben:
- S1/S2: Gegenseite legt auf / Anruf scheitert / Sprachkanal bricht ab →
  automatisch zurück zum Chat mit Hinweis (Fehler sichtbar statt still);
  leere Anrufansicht deutsch mit Zurück-Knopf (vorher englische Sackgasse
  ohne Zurück am Handy).
- S4: toter „Threads“-Knopf entfernt. S5: Space betreten (PC) öffnet
  letzten bzw. ersten Textkanal. S6: keine doppelte Anrufsteuerung am PC.
- Annehmen-Fenster mit Anrufer-Avatar und Anrufart; eigene Kachel mit
  eigenem Initial statt „D“; Mitgliederliste rund mit Profilbild und „(Du)“;
  Tastaturkürzel nur am PC; englische Texte in Anruf/Leerzuständen deutsch.
- Tests: Fehlerempfänger des Testrahmens nach app.main() zurück (vorher
  verschwanden Fehlermeldungen), Lesebestätigungs-Test im Wegwerf-Raum statt
  in der DM, robustere Schritte; test.ps1 räumt liegengebliebene
  Test-Prozesse weg; gemeinsame Hilfen integration_test/support/.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-09 11:35:40 +02:00
co-authored by Claude Opus 5.5
parent 842dd61031
commit 87b472a373
22 changed files with 934 additions and 378 deletions
+68 -8
View File
@@ -19,6 +19,7 @@ import 'package:pyramid/features/rooms/rooms_panel.dart';
import 'package:pyramid/features/spaces/spaces_rail.dart';
import 'package:pyramid/layout/mobile_page_stack.dart';
import 'package:pyramid/widgets/spring_press.dart' show motionDuration;
import 'package:pyramid/widgets/mxc_image.dart';
import 'package:pyramid/widgets/search_modal.dart';
import 'package:pyramid/widgets/settings_modal.dart';
import 'package:pyramid/widgets/share_target_dialog.dart';
@@ -151,6 +152,44 @@ class _AppShellState extends ConsumerState<AppShell>
ref.read(activeModalProvider.notifier).state = ModalKind.none;
}
bool _hadVoipCall = false;
bool _hadLiveKit = false;
/// Anruf ist vorbei: Anrufansicht verlassen (sofern kein anderer Anruf
/// mehr läuft) und kurz Bescheid geben.
void _leaveCallView(String message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
_leaveCallViewNow(message);
} catch (e, s) {
debugPrint('[AppShell] Anrufansicht verlassen: $e $s');
}
});
}
void _leaveCallViewNow(String message) {
{
if (!mounted) return;
final voip = ref.read(callSignalingProvider);
final lk = ref.read(voiceChannelProvider);
final stillInCall = (voip.currentCall != null &&
voip.currentCall!.state != CallState.kEnded) ||
lk.isActive ||
lk.isConnecting;
if (stillInCall) return;
if (ref.read(viewModeProvider) == ViewMode.voice) {
ref.read(viewModeProvider.notifier).state = ViewMode.chat;
}
ScaffoldMessenger.maybeOf(context)
?..hideCurrentSnackBar()
..showSnackBar(SnackBar(
content: Text(message),
behavior: SnackBarBehavior.floating,
duration: const Duration(seconds: 3),
));
}
}
@override
Widget build(BuildContext context) {
final pt = PyramidTheme.of(context);
@@ -164,6 +203,12 @@ class _AppShellState extends ConsumerState<AppShell>
// würde der Nutzer permanent zurück in die Call-UI gezwungen und könnte
// sie nie minimieren.
ref.listen(callSignalingProvider, (prev, next) {
// Anruf vorbei (aufgelegt, abgelehnt, gescheitert) → nicht auf der
// leeren Anrufansicht stehen lassen (UI-Audit S1, 2026-10-08).
final hasCall = next.currentCall != null &&
next.currentCall!.state != CallState.kEnded;
if (_hadVoipCall && !hasCall) _leaveCallView('Anruf beendet');
_hadVoipCall = hasCall;
final wasConnected = _wasVoipConnected;
final isConnected = next.currentCall != null &&
next.currentCall!.state == CallState.kConnected;
@@ -176,6 +221,12 @@ class _AppShellState extends ConsumerState<AppShell>
});
ref.listen(voiceChannelProvider, (prev, next) {
final lkActive = next.isActive || next.isConnecting;
if (_hadLiveKit && !lkActive) {
// Fehler (z. B. Verbindung verloren) sichtbar machen statt still.
_leaveCallView(next.error ?? (next.isVoiceChannel ? 'Sprachkanal verlassen' : 'Anruf beendet'));
}
_hadLiveKit = lkActive;
// Only auto-switch to voice view for video/audio calls, NOT voice channels
// (voice channels should stay in background with a mini bar)
final wasActive = _wasLiveKitActive;
@@ -519,18 +570,27 @@ class _IncomingCallOverlay extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: pt.accent,
shape: BoxShape.circle,
MxcAvatar(
mxcUri: call.room.avatar,
client: call.room.client,
size: 88,
placeholder: (_) => Container(
width: 88,
height: 88,
decoration: BoxDecoration(
color: pt.accent,
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
call.room.getLocalizedDisplayname().characters.firstOrNull?.toUpperCase() ?? '?',
style: const TextStyle(color: Colors.white, fontSize: 36, fontWeight: FontWeight.w700),
),
),
child: const Icon(Icons.person_rounded, size: 40, color: Colors.white),
),
const SizedBox(height: 24),
Text(
'Eingehender Anruf',
call.type == CallType.kVideo ? 'Eingehender Videoanruf' : 'Eingehender Anruf',
style: TextStyle(color: pt.fgDim, fontSize: 13),
),
const SizedBox(height: 8),