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:
@@ -1,4 +1,4 @@
|
||||
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-10
|
||||
// Version: 1.4.0 | Created: 2026-04-01 | Updated: 2026-07-04
|
||||
// Chat repository — bridges Matrix SDK timeline to app domain models.
|
||||
// Phase 2 additions: sendFile, sendReaction, redactEvent, reply support.
|
||||
|
||||
@@ -73,6 +73,17 @@ class ChatRepository {
|
||||
await room.sendFileEvent(file);
|
||||
}
|
||||
|
||||
/// Replaces the body of an existing text message (m.replace edit).
|
||||
Future<void> editMessage(
|
||||
String roomId,
|
||||
String eventId,
|
||||
String newText,
|
||||
) async {
|
||||
final room = _getRoom(roomId);
|
||||
if (room == null) return;
|
||||
await room.sendTextEvent(newText, editEventId: eventId);
|
||||
}
|
||||
|
||||
/// Sends an emoji reaction to [eventId].
|
||||
Future<void> sendReaction(String roomId, String eventId, String emoji) async {
|
||||
final room = _getRoom(roomId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Version: 1.1.0 | Created: 2026-04-01
|
||||
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-07-04
|
||||
// Riverpod providers for chat timeline, send, upload, react, reply.
|
||||
|
||||
import 'package:matrix/matrix.dart' show MatrixFile;
|
||||
@@ -84,6 +84,27 @@ class SendReaction extends _$SendReaction {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edits an existing text message.
|
||||
@riverpod
|
||||
class EditMessage extends _$EditMessage {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
Future<String?> edit(String roomId, String eventId, String newText) async {
|
||||
state = true;
|
||||
try {
|
||||
await ref
|
||||
.read(chatRepositoryProvider)
|
||||
.editMessage(roomId, eventId, newText);
|
||||
return null;
|
||||
} on Exception catch (e) {
|
||||
return e.toString();
|
||||
} finally {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes (redacts) a message by eventId.
|
||||
@riverpod
|
||||
class DeleteMessage extends _$DeleteMessage {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user