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,80 @@
// Version: 1.0.0 | Created: 2026-07-04
// Post-login prompt: if the account has secret storage (key backup) and this
// fresh device cannot read encrypted history yet, offer the recovery-key
// restore straight away instead of leaving it buried in Profile > Security.
//
// Shown at most once per login session. "Later" keeps the old behaviour
// (restore manually from the Profile tab).
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'key_restore_dialog.dart';
bool _promptedThisSession = false;
/// Reset when a new login happens (called from the login flow if needed).
void resetKeyRestorePrompt() => _promptedThisSession = false;
/// Checks whether the freshly logged-in session needs a key restore and,
/// if so, asks the user. Safe to call multiple times; only fires once.
Future<void> maybePromptKeyRestore(BuildContext context, Client client) async {
if (_promptedThisSession) return;
_promptedThisSession = true;
try {
// Wait for the first sync so account data (secret storage config) exists.
if (client.prevBatch == null) {
await client.onSync.stream.first.timeout(const Duration(seconds: 30));
}
} on TimeoutException {
debugPrint('[KeyRestorePrompt] first sync timed out — skipping');
return;
}
final enc = client.encryption;
if (enc == null) return;
// No secret storage on the account → nothing to restore from.
final hasSecretStorage =
client.accountData.containsKey('m.secret_storage.default_key');
if (!hasSecretStorage) return;
// Keys already cached (or session verified) → nothing to do.
var cached = false;
try {
cached = await enc.keyManager.isCached();
} on Exception catch (e) {
debugPrint('[KeyRestorePrompt] isCached failed: $e');
}
if (cached && !client.isUnknownSession) return;
if (!context.mounted) return;
final wantsRestore = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Restore encrypted messages?'),
content: const Text(
'This device cannot read your older encrypted messages yet. '
'Enter your recovery key now to unlock them, or do it later '
'from Profile > Security & Privacy.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Later'),
),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Enter recovery key'),
),
],
),
);
if (wantsRestore != true || !context.mounted) return;
await showKeyRestoreDialog(context, client);
}

View File

@@ -1,4 +1,4 @@
// Version: 1.4.1 | Created: 2026-04-01 | Updated: 2026-07-03
// Version: 1.5.0 | Created: 2026-04-01 | Updated: 2026-07-04
// Profile screen. Shows current user info and logout button.
import 'package:flutter/material.dart';
@@ -10,6 +10,8 @@ import '../../../core/config/app_config.dart';
import '../../../core/network/matrix_client.dart';
import '../../../shared/utils/matrix_id.dart';
import '../../../shared/widgets/matrix_avatar.dart';
import '../../panic/data/panic_repository.dart';
import '../../panic/presentation/panic_settings_dialog.dart';
import 'key_restore_dialog.dart';
import 'security_setup_dialog.dart';
@@ -147,6 +149,16 @@ class ProfileScreen extends ConsumerWidget {
),
],
),
ListTile(
leading: const Icon(Icons.emergency_share, color: Colors.red),
title: const Text('Emergency panic button'),
subtitle: Text(
ref.watch(panicRepositoryProvider).config != null
? 'Armed. Hold the alert icon on the rooms screen to trigger.'
: 'Send your location and an alert to a room of your choice',
),
onTap: () => showPanicSettingsDialog(context, ref),
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),