From 55ecbc3590baeff0f898e354c0b29e7b41bf9e10 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Mon, 30 Mar 2026 12:47:49 -0400 Subject: [PATCH] Add better error handling, send messages early and update when delivered --- lib/controllers/client_controller.dart | 12 ++- lib/controllers/message_controller.dart | 1 + lib/controllers/room_chat_controller.dart | 40 ++++---- lib/models/configs/message_config.dart | 10 -- lib/widgets/chat_page/room_chat.dart | 12 ++- .../chat_page/wrappers/message_wrapper.dart | 97 ++++++++++++------- .../wrappers/text_message_wrapper.dart | 4 +- pubspec.lock | 8 ++ pubspec.yaml | 1 + 9 files changed, 111 insertions(+), 74 deletions(-) diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index d8315c4..ced57f7 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -9,6 +9,7 @@ import "package:flutter/foundation.dart"; import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/init_complete_controller.dart"; +import "package:nexus/controllers/new_events_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/space_edges_controller.dart"; import "package:nexus/controllers/sync_status_controller.dart"; @@ -74,6 +75,13 @@ class ClientController extends AsyncNotifier { case "init_complete": ref.watch(InitCompleteController.provider.notifier).complete(); break; + case "send_complete": + final event = Event.fromJson(decodedMuksEvent["event"]); + + ref + .watch(NewEventsController.provider(event.roomId).notifier) + .add(IList([event])); + break; case "sync_complete": final syncData = SyncData.fromJson(decodedMuksEvent); final roomProvider = RoomsController.provider; @@ -150,8 +158,8 @@ class ClientController extends AsyncNotifier { Future redactEvent(RedactEventRequest report) => _sendCommand("redact_event", report.toJson()); - Future sendMessage(SendMessageRequest request) => - _sendCommand("send_message", request.toJson()); + Future sendMessage(SendMessageRequest request) async => + Event.fromJson(await _sendCommand("send_message", request.toJson())); Future verify(String recoveryKey) async { try { diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index 51e4287..675a6e5 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -46,6 +46,7 @@ class MessageController extends AsyncNotifier { "big": event.localContent?.bigEmoji == true, "eventType": type, "pmp": event.content["com.beeper.per_message_profile"], + "error": event.sendError, "editSource": event.localContent?.editSource ?? newContent?["body"] ?? diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 6d5ac7d..6512031 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -28,7 +28,6 @@ class RoomChatController extends AsyncNotifier { final client = ref.watch(ClientController.provider.notifier); var room = ref.read(RoomsController.provider)[roomId]; if (room == null) return InMemoryChatController(); - final state = await client.getRoomState( GetRoomStateRequest(roomId: roomId), ); @@ -78,7 +77,6 @@ class RoomChatController extends AsyncNotifier { ref.onDispose( ref.listen(NewEventsController.provider(roomId), (_, next) async { - final controller = await future; for (final event in next) { if (event.type == "m.room.redaction") { final controller = await future; @@ -116,12 +114,8 @@ class RoomChatController extends AsyncNotifier { ), ); } - if (message != null && - !controller.messages.any( - (oldMessage) => oldMessage.id == message.id, - ) && - ref.mounted) { - await controller.insertMessage(message); + if (message != null && ref.mounted) { + await insertMessage(message); } } } @@ -152,19 +146,11 @@ class RoomChatController extends AsyncNotifier { : controller.updateMessage(oldMessage, message); } - Future deleteMessage(Message message, {String? reason}) async { - final controller = await future; - await controller.removeMessage(message); - await ref - .watch(ClientController.provider.notifier) - .redactEvent( - RedactEventRequest( - eventId: message.id, - roomId: roomId, - reason: reason, - ), - ); - } + Future deleteMessage(Message message, {String? reason}) => ref + .watch(ClientController.provider.notifier) + .redactEvent( + RedactEventRequest(eventId: message.id, roomId: roomId, reason: reason), + ); Future loadOlder([InMemoryChatController? chatController]) async { final response = await ref @@ -242,7 +228,8 @@ class RoomChatController extends AsyncNotifier { } final client = ref.watch(ClientController.provider.notifier); - client.sendMessage( + final room = ref.read(RoomsController.provider)[roomId]; + final event = await client.sendMessage( SendMessageRequest( roomId: roomId, mentions: Mentions( @@ -260,6 +247,15 @@ class RoomChatController extends AsyncNotifier { : Relation(eventId: relation.id, relationType: relationType), ), ); + final message = room == null + ? null + : await ref.watch( + MessageController.provider( + MessageConfig(room: room, event: event), + ).future, + ); + + if (message != null) insertMessage(message); } Future resolveUser(String id) async { diff --git a/lib/models/configs/message_config.dart b/lib/models/configs/message_config.dart index 9020f78..f7490e5 100644 --- a/lib/models/configs/message_config.dart +++ b/lib/models/configs/message_config.dart @@ -6,7 +6,6 @@ part "message_config.g.dart"; @freezed abstract class MessageConfig with _$MessageConfig { - const MessageConfig._(); const factory MessageConfig({ @Default(false) bool alwaysReturn, @Default(false) bool includeEdits, @@ -14,15 +13,6 @@ abstract class MessageConfig with _$MessageConfig { required Event event, }) = _MessageConfig; - @override - bool operator ==(Object other) => - other.runtimeType == runtimeType && - other is MessageConfig && - other.event.eventId == event.eventId; - - @override - int get hashCode => Object.hash(runtimeType, event.eventId); - factory MessageConfig.fromJson(Map json) => _$MessageConfigFromJson(json); } diff --git a/lib/widgets/chat_page/room_chat.dart b/lib/widgets/chat_page/room_chat.dart index 6a14795..a30a145 100644 --- a/lib/widgets/chat_page/room_chat.dart +++ b/lib/widgets/chat_page/room_chat.dart @@ -21,7 +21,7 @@ import "package:nexus/widgets/chat_page/room_appbar.dart"; import "package:nexus/widgets/chat_page/wrappers/text_message_wrapper.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart"; import "package:nexus/widgets/form_text_input.dart"; -// import "package:dynamic_polls/dynamic_polls.dart"; +import "package:nexus/main.dart"; class RoomChat extends HookConsumerWidget { final bool isDesktop; @@ -108,11 +108,13 @@ class RoomChat extends HookConsumerWidget { ), TextButton( onPressed: () async { - notifier.deleteMessage( - message, - reason: deleteReasonController.text, - ); Navigator.of(context).pop(); + await notifier + .deleteMessage( + message, + reason: deleteReasonController.text, + ) + .onError(showError); }, child: Text("Delete"), ), diff --git a/lib/widgets/chat_page/wrappers/message_wrapper.dart b/lib/widgets/chat_page/wrappers/message_wrapper.dart index b6af370..216d68d 100644 --- a/lib/widgets/chat_page/wrappers/message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/message_wrapper.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_avatar.dart"; import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; +import "package:timeago/timeago.dart"; class MessageWrapper extends StatelessWidget { final Message message; @@ -10,41 +11,69 @@ class MessageWrapper extends StatelessWidget { const MessageWrapper(this.message, this.child, this.groupStatus, {super.key}); @override - Widget build(BuildContext context) => ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(12)), - child: AnimatedContainer( - padding: message.metadata?["flashing"] == true - ? EdgeInsets.all(8) - : EdgeInsets.all(0), - color: message.metadata?["flashing"] == true - ? Theme.of(context).colorScheme.onSurface.withAlpha(50) - : Colors.transparent, - duration: Duration(milliseconds: 250), - child: Row( - spacing: 8, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - groupStatus?.isFirst != false - ? MessageAvatar(message, height: 40) - : SizedBox(width: 40), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 4, - children: [ - if (groupStatus?.isFirst != false) - MessageDisplayname( - message, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + Widget build(BuildContext context) { + final theme = Theme.of(context); + final error = message.metadata?["error"]; + return ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(12)), + child: AnimatedContainer( + padding: message.metadata?["flashing"] == true + ? EdgeInsets.all(8) + : EdgeInsets.all(0), + color: message.metadata?["flashing"] == true + ? Theme.of(context).colorScheme.onSurface.withAlpha(50) + : Colors.transparent, + duration: Duration(milliseconds: 250), + child: Row( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + groupStatus?.isFirst != false + ? MessageAvatar(message, height: 40) + : SizedBox(width: 40), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + if (groupStatus?.isFirst != false) + Row( + children: [ + Flexible( + child: MessageDisplayname( + message, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + if (message.deliveredAt != null && + groupStatus?.isFirst == true) + Tooltip( + message: message.deliveredAt!.toString(), + child: Text( + format(message.deliveredAt!), + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.grey, + ), + ), + ), + ], ), - ), - child, - ], + child, + if (error != null && error != "not sent") + Text( + error, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ], + ), ), - ), - ], + ], + ), ), - ), - ); + ); + } } diff --git a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart index d4e8ed9..63329b9 100644 --- a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart @@ -40,7 +40,9 @@ class TextMessageWrapper extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: isSentByMe - ? colorScheme.primaryContainer + ? (message.id.startsWith("~") + ? colorScheme.onPrimary + : colorScheme.primaryContainer) : colorScheme.surfaceContainer, ), child: Column( diff --git a/pubspec.lock b/pubspec.lock index de807aa..e50446c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1357,6 +1357,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.0+1" + timeago: + dependency: "direct main" + description: + name: timeago + sha256: b05159406a97e1cbb2b9ee4faa9fb096fe0e2dfcd8b08fcd2a00553450d3422e + url: "https://pub.dev" + source: hosted + version: "3.7.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 80c3687..b290a45 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: hooks: ^1.0.0 code_assets: ^1.0.0 ffigen: ^20.1.1 + timeago: ^3.7.1 dev_dependencies: build_runner: ^2.4.11