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,4 +1,4 @@
// Version: 1.3.1 | Created: 2026-04-01 | Updated: 2026-07-03
// Version: 1.4.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Full chat screen — timeline + input + typing indicators + read receipts
// + long-press context menu (reply, react, copy, delete) + room options sheet.
@@ -217,6 +217,12 @@ class _MessageWithGestures extends ConsumerWidget {
context,
).showSnackBar(const SnackBar(content: Text('Message copied.')));
},
onEdit: message.isMine && message.body != null
? () {
Navigator.pop(context);
_showEditDialog(context, ref);
}
: null,
onDelete: message.isMine
? () async {
Navigator.pop(context);
@@ -229,6 +235,44 @@ class _MessageWithGestures extends ConsumerWidget {
);
}
void _showEditDialog(BuildContext context, WidgetRef ref) {
final controller = TextEditingController(text: message.body ?? '');
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Edit message'),
content: TextField(
controller: controller,
autofocus: true,
maxLines: null,
decoration: const InputDecoration(border: OutlineInputBorder()),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () async {
final newText = controller.text.trim();
Navigator.of(ctx).pop();
if (newText.isEmpty || newText == message.body) return;
final error = await ref
.read(editMessageProvider.notifier)
.edit(roomId, message.eventId, newText);
if (error != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Edit failed: $error')),
);
}
},
child: const Text('Save'),
),
],
),
);
}
void _showEmojiPicker(BuildContext context, WidgetRef ref) {
showModalBottomSheet<void>(
context: context,
@@ -437,6 +481,7 @@ class _MessageContextMenu extends StatelessWidget {
required this.onReply,
required this.onReact,
required this.onCopy,
this.onEdit,
this.onDelete,
});
@@ -445,6 +490,7 @@ class _MessageContextMenu extends StatelessWidget {
final VoidCallback onReply;
final VoidCallback onReact;
final VoidCallback onCopy;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
@override
@@ -469,6 +515,12 @@ class _MessageContextMenu extends StatelessWidget {
title: const Text('Copy text'),
onTap: onCopy,
),
if (onEdit != null)
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text('Edit'),
onTap: onEdit,
),
if (onDelete != null)
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),