fix: Haken live, Einladung im Chat annehmen, Trenner verschwindet zuverlässig

Bernds Test mit der PC-Testinstanz:
- ✓✓ erschien erst nach erneutem Öffnen: eine Lesebestätigung ohne neue
  Nachricht zeichnete nichts neu (Raum-Update kommt im SDK vor der
  Bestätigung). Jetzt nach jedem Sync Fingerabdruck der Lesepositionen
  prüfen und bei Änderung neu zeichnen.
- Einladung: Chat zeigte leeren Verlauf, Annehmen ging nur in der Liste, und
  danach blieb der Chat leer bis zum App-Neustart. Jetzt Annehmen/Ablehnen
  direkt im Chat; Annehmen wartet auf den Beitritts-Sync, der Verlauf baut
  sich bei invite→join selbst neu auf.
- Trenner blieb stehen, wenn nach dem Öffnen/Zurückkommen nicht gescrollt
  wurde. Jetzt: neueste Nachrichten sichtbar (≤ halbe Bildschirmhöhe
  hochgescrollt) → nach 6 s weich ausblenden, auch ohne Scrollen.
- Live-Test mit beiden Testkonten (Gegenprobe: ohne Fix rot) in
  scripts/test.ps1 -Ui; Testlauf-Sperre erkennt nur noch eigene Testläufe.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 23:33:59 +02:00
co-authored by Claude Opus 5.5
parent a6461fe23c
commit c817de924a
7 changed files with 479 additions and 16 deletions
+139
View File
@@ -151,3 +151,142 @@ class _FloatingComposerArea extends StatelessWidget {
);
}
}
/// Einladung in diesen Chat (z. B. aus einer Benachrichtigung geöffnet):
/// Annehmen/Ablehnen direkt hier statt eines leeren Verlaufs – vorher ging
/// das nur in der Chatliste (Bernd 2026-10-07).
class _InvitePanel extends ConsumerStatefulWidget {
final Room room;
final PyramidTheme pt;
const _InvitePanel({required this.room, required this.pt});
@override
ConsumerState<_InvitePanel> createState() => _InvitePanelState();
}
class _InvitePanelState extends ConsumerState<_InvitePanel> {
bool _busy = false;
String? _error;
String get _inviter {
final me = widget.room.client.userID;
final sender = me == null
? null
: widget.room.getState(EventTypes.RoomMember, me)?.senderId;
if (sender == null) return 'Jemand';
return widget.room.unsafeGetUserFromMemoryOrFallback(sender).calcDisplayname();
}
Future<void> _accept() async {
setState(() {
_busy = true;
_error = null;
});
try {
await acceptInvite(ref, widget.room);
} catch (_) {
if (mounted) {
setState(() {
_busy = false;
_error = 'Annehmen hat nicht geklappt – bitte noch einmal versuchen.';
});
}
}
}
Future<void> _decline() async {
setState(() {
_busy = true;
_error = null;
});
try {
await widget.room.leave();
if (mounted) ref.read(activeRoomIdProvider.notifier).state = null;
} catch (_) {
if (mounted) {
setState(() {
_busy = false;
_error = 'Ablehnen hat nicht geklappt – bitte noch einmal versuchen.';
});
}
}
}
@override
Widget build(BuildContext context) {
final pt = widget.pt;
final isDm = widget.room.isDirectChat;
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(28),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isDm ? Icons.person_add_alt_1_rounded : Icons.group_add_rounded,
size: 48,
color: pt.accent,
),
const SizedBox(height: 16),
Text(
isDm
? '$_inviter möchte mit dir chatten'
: '$_inviter lädt dich in „${widget.room.getLocalizedDisplayname()}“ ein',
textAlign: TextAlign.center,
style: TextStyle(
color: pt.fg,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Nach dem Annehmen siehst du die Nachrichten und kannst antworten.',
textAlign: TextAlign.center,
style: TextStyle(color: pt.fgMuted, fontSize: 13.5),
),
const SizedBox(height: 24),
if (_busy)
SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(strokeWidth: 2.5, color: pt.accent),
)
else
Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
OutlinedButton(
onPressed: _decline,
style: OutlinedButton.styleFrom(
foregroundColor: pt.fgMuted,
side: BorderSide(color: pt.border),
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
),
child: const Text('Ablehnen'),
),
FilledButton(
onPressed: _accept,
style: FilledButton.styleFrom(
backgroundColor: pt.accent,
foregroundColor: pt.accentFg,
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 14),
),
child: const Text('Annehmen'),
),
],
),
if (_error != null) ...[
const SizedBox(height: 14),
Text(_error!,
textAlign: TextAlign.center,
style: TextStyle(color: pt.danger, fontSize: 13)),
],
],
),
),
);
}
}