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>
167 lines
5.0 KiB
Dart
167 lines
5.0 KiB
Dart
// Version: 1.0.0 | Created: 2026-07-04
|
|
// Panic button settings — choose the alert room and message.
|
|
// Opened from Profile > Emergency panic button.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:matrix/matrix.dart';
|
|
|
|
import '../../../core/network/matrix_client.dart';
|
|
import '../data/panic_repository.dart';
|
|
|
|
Future<void> showPanicSettingsDialog(BuildContext context, WidgetRef ref) {
|
|
return showDialog<void>(
|
|
context: context,
|
|
builder: (_) => const _PanicSettingsDialog(),
|
|
);
|
|
}
|
|
|
|
class _PanicSettingsDialog extends ConsumerStatefulWidget {
|
|
const _PanicSettingsDialog();
|
|
|
|
@override
|
|
ConsumerState<_PanicSettingsDialog> createState() =>
|
|
_PanicSettingsDialogState();
|
|
}
|
|
|
|
class _PanicSettingsDialogState extends ConsumerState<_PanicSettingsDialog> {
|
|
String? _roomId;
|
|
late final TextEditingController _messageController;
|
|
bool _saving = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final cfg = ref.read(panicRepositoryProvider).config;
|
|
_roomId = cfg?.roomId;
|
|
_messageController = TextEditingController(
|
|
text: cfg?.message ??
|
|
'EMERGENCY: I need help. This is an automated alert from M8Chat.',
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_messageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
final roomId = _roomId;
|
|
if (roomId == null) return;
|
|
setState(() => _saving = true);
|
|
try {
|
|
await ref.read(panicRepositoryProvider).saveConfig(
|
|
roomId: roomId,
|
|
message: _messageController.text.trim(),
|
|
);
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Panic button armed. Hold the alert icon '
|
|
'on the rooms screen to trigger it.'),
|
|
),
|
|
);
|
|
}
|
|
} on Exception catch (e) {
|
|
if (mounted) {
|
|
setState(() => _saving = false);
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('Could not save: $e')));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _disable() async {
|
|
setState(() => _saving = true);
|
|
await ref.read(panicRepositoryProvider).clearConfig();
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Panic button disabled.')),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final client = ref.watch(matrixClientProvider);
|
|
final rooms = client.rooms
|
|
.where((r) => r.membership == Membership.join)
|
|
.toList()
|
|
..sort((a, b) => a
|
|
.getLocalizedDisplayname()
|
|
.toLowerCase()
|
|
.compareTo(b.getLocalizedDisplayname().toLowerCase()));
|
|
final hasConfig = ref.read(panicRepositoryProvider).config != null;
|
|
|
|
return AlertDialog(
|
|
title: const Text('Emergency panic button'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
'When you hold the alert icon, M8Chat sends this message '
|
|
'and your current location to the room you choose here.',
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<String>(
|
|
initialValue:
|
|
rooms.any((r) => r.id == _roomId) ? _roomId : null,
|
|
isExpanded: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Send alert to room',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: [
|
|
for (final room in rooms)
|
|
DropdownMenuItem(
|
|
value: room.id,
|
|
child: Text(
|
|
room.getLocalizedDisplayname(),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
onChanged: (v) => setState(() => _roomId = v),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: _messageController,
|
|
maxLines: 3,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Alert message',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
if (hasConfig)
|
|
TextButton(
|
|
onPressed: _saving ? null : _disable,
|
|
child: const Text('Disable', style: TextStyle(color: Colors.red)),
|
|
),
|
|
TextButton(
|
|
onPressed: _saving ? null : () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton(
|
|
onPressed: _saving || _roomId == null ? null : _save,
|
|
child: _saving
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Text('Save'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|