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

@@ -0,0 +1,70 @@
// Version: 1.0.0 | Created: 2026-07-04
// Panic trigger button for the rooms AppBar. Push-to-talk style:
// HOLD to fire the alert; a plain tap only shows the hint. Only rendered
// when a panic configuration exists in account data.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/panic_repository.dart';
class PanicButton extends ConsumerStatefulWidget {
const PanicButton({super.key});
@override
ConsumerState<PanicButton> createState() => _PanicButtonState();
}
class _PanicButtonState extends ConsumerState<PanicButton> {
bool _sending = false;
Future<void> _trigger() async {
if (_sending) return;
setState(() => _sending = true);
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
const SnackBar(content: Text('Sending emergency alert…')),
);
final error = await ref.read(panicRepositoryProvider).trigger();
if (!mounted) return;
setState(() => _sending = false);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
backgroundColor: error == null ? Colors.green.shade800 : null,
content: Text(
error ?? 'Emergency alert sent with your location.',
),
),
);
}
@override
Widget build(BuildContext context) {
// Hide when not configured. Config lives in account data, which is
// available once sync has run; rebuilds happen with the parent screen.
final configured = ref.watch(panicRepositoryProvider).config != null;
if (!configured) return const SizedBox.shrink();
return GestureDetector(
onLongPress: _trigger,
child: IconButton(
tooltip: 'Hold to send emergency alert',
icon: _sending
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.emergency_share, color: Colors.red),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Hold the button to send the emergency alert.'),
),
);
},
),
);
}
}