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>
126 lines
3.1 KiB
Dart
126 lines
3.1 KiB
Dart
// 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;
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../data/chat_repository.dart';
|
|
import '../domain/message_model.dart';
|
|
|
|
part 'chat_controller.g.dart';
|
|
|
|
/// Streams the message list for [roomId].
|
|
@riverpod
|
|
Stream<List<MessageModel>> chatTimeline(Ref ref, String roomId) {
|
|
final repo = ref.watch(chatRepositoryProvider);
|
|
return repo.watchTimeline(roomId);
|
|
}
|
|
|
|
/// Sends a text message. Returns an error string on failure, null on success.
|
|
/// Also handles sending replies when [inReplyToEventId] is set.
|
|
@riverpod
|
|
class SendMessage extends _$SendMessage {
|
|
@override
|
|
bool build() => false; // isSending
|
|
|
|
Future<String?> send(
|
|
String roomId,
|
|
String text, {
|
|
String? inReplyToEventId,
|
|
}) async {
|
|
if (text.trim().isEmpty) return null;
|
|
state = true;
|
|
try {
|
|
await ref
|
|
.read(chatRepositoryProvider)
|
|
.sendTextMessage(roomId, text, inReplyToEventId: inReplyToEventId);
|
|
return null;
|
|
} on Exception catch (e) {
|
|
return e.toString();
|
|
} finally {
|
|
state = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Uploads a file and sends it as a room message.
|
|
/// State: null = idle, empty string = uploading, non-empty = error message.
|
|
@riverpod
|
|
class UploadFile extends _$UploadFile {
|
|
@override
|
|
String? build() => null; // null = idle
|
|
|
|
Future<void> upload(String roomId, MatrixFile file) async {
|
|
state = ''; // uploading
|
|
try {
|
|
await ref.read(chatRepositoryProvider).sendFile(roomId, file);
|
|
state = null; // success — back to idle
|
|
} on Exception catch (e) {
|
|
state = e.toString(); // error
|
|
}
|
|
}
|
|
|
|
void clearError() => state = null;
|
|
}
|
|
|
|
/// Sends an emoji reaction to [eventId].
|
|
@riverpod
|
|
class SendReaction extends _$SendReaction {
|
|
@override
|
|
bool build() => false;
|
|
|
|
Future<String?> react(String roomId, String eventId, String emoji) async {
|
|
state = true;
|
|
try {
|
|
await ref
|
|
.read(chatRepositoryProvider)
|
|
.sendReaction(roomId, eventId, emoji);
|
|
return null;
|
|
} on Exception catch (e) {
|
|
return e.toString();
|
|
} finally {
|
|
state = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 {
|
|
@override
|
|
bool build() => false;
|
|
|
|
Future<String?> delete(String roomId, String eventId) async {
|
|
state = true;
|
|
try {
|
|
await ref.read(chatRepositoryProvider).redactEvent(roomId, eventId);
|
|
return null;
|
|
} on Exception catch (e) {
|
|
return e.toString();
|
|
} finally {
|
|
state = false;
|
|
}
|
|
}
|
|
}
|