feat: encrypted-history prompt, incoming-call alerts, message edit, panic button (v1.7.0+11)

P1 key_restore_prompt: offer recovery-key restore right after login
P2 incoming calls: mount overlay in app.dart + detect MSC3401 call.member
   (Element X signalling) — calls were previously invisible
P3 message edit: editMessage/EditMessage + Edit dialog (reactions/delete
   were already wired)
P5 panic button: new feature/panic module — hold-to-send alert text +
   m.location to a configured room; alert sent before geolocation so a
   denied permission never blocks it

All verified end-to-end in headless Chromium against production build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 07:30:57 +10:00
parent 923c0ad878
commit a36df1c961
15 changed files with 700 additions and 30 deletions

View File

@@ -1,10 +1,10 @@
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-02
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-07-04
// IncomingCallOverlay — full-screen overlay shown when an m.call.invite
// arrives. Displays caller name/avatar, and Accept / Decline buttons.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../app/router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../shared/widgets/matrix_avatar.dart';
@@ -58,20 +58,27 @@ class IncomingCallOverlayHost extends ConsumerWidget {
}
}
class _IncomingCallOverlay extends StatefulWidget {
class _IncomingCallOverlay extends ConsumerStatefulWidget {
const _IncomingCallOverlay({required this.call});
final IncomingCall call;
@override
State<_IncomingCallOverlay> createState() => _IncomingCallOverlayState();
ConsumerState<_IncomingCallOverlay> createState() =>
_IncomingCallOverlayState();
}
class _IncomingCallOverlayState extends State<_IncomingCallOverlay> {
class _IncomingCallOverlayState extends ConsumerState<_IncomingCallOverlay> {
bool _dismissed = false;
String? _dismissedForCallId;
@override
Widget build(BuildContext context) {
// A new call resets the dismissal so the overlay can show again.
if (_dismissedForCallId != widget.call.callId) {
_dismissed = false;
_dismissedForCallId = widget.call.callId;
}
if (_dismissed) return const SizedBox.shrink();
final theme = Theme.of(context);
@@ -129,9 +136,13 @@ class _IncomingCallOverlayState extends State<_IncomingCallOverlay> {
label: 'Accept',
colour: Colors.green,
onTap: () {
context.push(
'/calls/${Uri.encodeComponent(call.roomId)}',
);
// The overlay lives above the router's navigator, so
// context.push would not find GoRouter — use the
// router instance directly, then dismiss the overlay.
setState(() => _dismissed = true);
ref
.read(routerProvider)
.push('/calls/${Uri.encodeComponent(call.roomId)}');
},
),
],