From 4087d6ca11296ed9f61aec50a424cd78cf514d54 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 18 Mar 2026 22:06:11 -0400 Subject: [PATCH] dont fail to parse on invalid reply --- lib/controllers/message_controller.dart | 357 ++++++++++++------------ 1 file changed, 183 insertions(+), 174 deletions(-) diff --git a/lib/controllers/message_controller.dart b/lib/controllers/message_controller.dart index 615306f..74c9473 100644 --- a/lib/controllers/message_controller.dart +++ b/lib/controllers/message_controller.dart @@ -14,150 +14,169 @@ class MessageController extends AsyncNotifier { @override Future build() async { - if (config.event.relationType == "m.replace" && !config.includeEdits) { - return null; - } - final client = ref.watch(ClientController.provider.notifier); + try { + if (config.event.relationType == "m.replace" && !config.includeEdits) { + return null; + } + final client = ref.watch(ClientController.provider.notifier); - if (!ref.mounted) return null; - final event = config.event.lastEditRowId == null - ? config.event - : config.room.events.firstWhereOrNull( - (e) => e.rowId == config.event.lastEditRowId, - ) ?? - config.event; + if (!ref.mounted) return null; + final event = config.event.lastEditRowId == null + ? config.event + : config.room.events.firstWhereOrNull( + (e) => e.rowId == config.event.lastEditRowId, + ) ?? + config.event; - final replyId = - config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; - final replyEvent = replyId == null - ? null - : await client.getEvent( - GetEventRequest(room: config.room, eventId: replyId), - ); + final replyId = + config.event.content["m.relates_to"]?["m.in_reply_to"]?["event_id"]; + final replyEvent = replyId == null + ? null + : await client + .getEvent(GetEventRequest(room: config.room, eventId: replyId)) + .onError((_, _) => null); - if (!ref.mounted) return null; + if (!ref.mounted) return null; - final members = ref.watch(MembersController.provider(config.room)); - final author = members.firstWhereOrNull( - (member) => member.stateKey == event.authorId, - ); - if (!ref.mounted) return null; + final members = ref.watch(MembersController.provider(config.room)); + final author = members.firstWhereOrNull( + (member) => member.stateKey == event.authorId, + ); + if (!ref.mounted) return null; - final content = (event.decrypted ?? event.content); - final type = (config.event.decryptedType ?? config.event.type); - final newContent = content["m.new_content"] as Map?; + final content = (event.decrypted ?? event.content); + final type = (config.event.decryptedType ?? config.event.type); + final newContent = content["m.new_content"] as Map?; - final homeserver = ref.read(ClientStateController.provider)?.homeserverUrl; - final source = homeserver == null || content["url"] == null - ? "null" - : Uri.parse(content["url"]).mxcToHttps(homeserver).toString(); + final homeserver = ref + .read(ClientStateController.provider) + ?.homeserverUrl; + final source = homeserver == null || content["url"] == null + ? "null" + : Uri.parse(content["url"]).mxcToHttps(homeserver).toString(); - final metadata = { - "body": config.event.redactedBy == null - ? (newContent?["body"] ?? content["body"] ?? "") - : "Deleted Message", - if (replyEvent != null) - "reply": await ref.watch( - MessageController.provider( - MessageConfig( - event: replyEvent, - room: config.room, - alwaysReturn: true, - ), - ).future, + final metadata = { + "body": config.event.redactedBy == null + ? (newContent?["body"] ?? content["body"] ?? "") + : "Deleted Message", + if (replyEvent != null) + "reply": await ref.watch( + MessageController.provider( + MessageConfig( + event: replyEvent, + room: config.room, + alwaysReturn: true, + ), + ).future, + ), + "flashing": false, + "timelineId": event.timelineRowId, + "big": event.localContent?.bigEmoji == true, + "eventType": type, + "avatarUrl": author?.content["avatar_url"], + "editSource": + event.localContent?.editSource ?? + newContent?["body"] ?? + content["body"], + "displayName": author?.content["displayname"]?.isNotEmpty == true + ? author?.content["displayname"] + : event.authorId.substring(1).split(":")[0], + "txnId": config.event.transactionId, + }; + + if (!ref.mounted) return null; + + final editedAt = event.relationType == "m.replace" + ? event.timestamp + : null; + + if ((event.redactedBy != null && !config.alwaysReturn) || + (!config.includeEdits && + (config.event.relationType == "m.replace"))) { + return null; + } + + // TODO: Use server-generated preview if enabled + + // final match = Uri.tryParse( + // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", + // ); + + final asText = + Message.text( + metadata: metadata, + id: config.event.eventId, + authorId: event.authorId, + text: + newContent?["formatted_body"] ?? + newContent?["body"] ?? + content["formatted_body"] ?? + content["body"] ?? + "", + replyToMessageId: replyId, + deliveredAt: config.event.timestamp, + editedAt: editedAt, + ) + as TextMessage; + + return switch (type) { + "m.room.encrypted" => asText.copyWith( + text: "Unable to decrypt message.", + metadata: {...metadata, "body": "Unable to decrypt message."}, ), - "flashing": false, - "timelineId": event.timelineRowId, - "big": event.localContent?.bigEmoji == true, - "eventType": type, - "avatarUrl": author?.content["avatar_url"], - "editSource": - event.localContent?.editSource ?? - newContent?["body"] ?? - content["body"], - "displayName": author?.content["displayname"]?.isNotEmpty == true - ? author?.content["displayname"] - : event.authorId.substring(1).split(":")[0], - "txnId": config.event.transactionId, - }; - - if (!ref.mounted) return null; - - final editedAt = event.relationType == "m.replace" ? event.timestamp : null; - - if ((event.redactedBy != null && !config.alwaysReturn) || - (!config.includeEdits && (config.event.relationType == "m.replace"))) { - return null; - } - - // TODO: Use server-generated preview if enabled - - // final match = Uri.tryParse( - // RegExp(regexLink, caseSensitive: false).firstMatch(body)?.group(0) ?? "", - // ); - - final asText = - Message.text( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - text: - newContent?["formatted_body"] ?? - newContent?["body"] ?? - content["formatted_body"] ?? - content["body"] ?? - "", - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - editedAt: editedAt, - ) - as TextMessage; - - return switch (type) { - "m.room.encrypted" => asText.copyWith( - text: "Unable to decrypt message.", - metadata: {...metadata, "body": "Unable to decrypt message."}, - ), - // "org.matrix.msc3381.poll.start" => Message.custom( - // metadata: { - // ...metadata, - // "poll": event.parsedPollEventContent.pollStartContent, - // "responses": event.getPollResponses(timeline), - // }, - // id: eventId, - // deliveredAt: originServerTs, - // authorId: senderId, - // ), - ("m.sticker" || "m.room.message") => switch (content["msgtype"]) { - null || "m.image" => Message.image( - id: "${config.event.eventId}-image", - authorId: event.authorId, - source: source, - replyToMessageId: replyId, - metadata: metadata, - text: asText.text, - deliveredAt: config.event.timestamp, - blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], - ), - "m.audio" || "m.file" => Message.file( - name: content["filename"].toString(), - size: content["info"]["size"], - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - source: source, - replyToMessageId: replyId, - deliveredAt: config.event.timestamp, - ), - _ => asText, - }, - "m.room.member" => - content["membership"] == event.unsigned["prev_content"]?["membership"] - ? null - : Message.system( - metadata: { - ...metadata, - "body": + // "org.matrix.msc3381.poll.start" => Message.custom( + // metadata: { + // ...metadata, + // "poll": event.parsedPollEventContent.pollStartContent, + // "responses": event.getPollResponses(timeline), + // }, + // id: eventId, + // deliveredAt: originServerTs, + // authorId: senderId, + // ), + ("m.sticker" || "m.room.message") => switch (content["msgtype"]) { + null || "m.image" => Message.image( + id: config.event.eventId, + authorId: event.authorId, + source: source, + replyToMessageId: replyId, + metadata: metadata, + text: asText.text, + deliveredAt: config.event.timestamp, + blurhash: (content["info"] as Map?)?["xyz.amorgan.blurhash"], + ), + "m.audio" || "m.file" => Message.file( + name: content["filename"].toString(), + size: content["info"]["size"], + metadata: metadata, + id: config.event.eventId, + authorId: event.authorId, + source: source, + replyToMessageId: replyId, + deliveredAt: config.event.timestamp, + ), + _ => asText, + }, + "m.room.member" => + content["membership"] == event.unsigned["prev_content"]?["membership"] + ? null + : Message.system( + metadata: { + ...metadata, + "body": + "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { + "invite" => "was invited to", + "join" => "joined", + "leave" => "left", + "knock" => "asked to join", + "ban" => "was banned from", + _ => "did something relating to", + }} the room.", + }, + id: config.event.eventId, + authorId: event.authorId, + deliveredAt: config.event.timestamp, + text: "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { "invite" => "was invited to", "join" => "joined", @@ -166,45 +185,35 @@ class MessageController extends AsyncNotifier { "ban" => "was banned from", _ => "did something relating to", }} the room.", - }, - id: config.event.eventId, - authorId: event.authorId, - deliveredAt: config.event.timestamp, - text: - "${content["displayname"] ?? event.stateKey} ${switch (content["membership"]) { - "invite" => "was invited to", - "join" => "joined", - "leave" => "left", - "knock" => "asked to join", - "ban" => "was banned from", - _ => "did something relating to", - }} the room.", - ), + ), - "m.room.redaction" => - config.alwaysReturn - ? asText.copyWith( - metadata: { - ...(asText.metadata ?? {}), - "body": "Deleted Message", - }, - ) - : null, - _ => - config.alwaysReturn - ? asText - : ( - // Turn this on for debugging purposes - false - // ignore: dead_code - ? Message.unsupported( - metadata: metadata, - id: config.event.eventId, - authorId: event.authorId, - replyToMessageId: replyId, - ) - : null), - }; + "m.room.redaction" => + config.alwaysReturn + ? asText.copyWith( + metadata: { + ...(asText.metadata ?? {}), + "body": "Deleted Message", + }, + ) + : null, + _ => + config.alwaysReturn + ? asText + : ( + // Turn this on for debugging purposes + false + // ignore: dead_code + ? Message.unsupported( + metadata: metadata, + id: config.event.eventId, + authorId: event.authorId, + replyToMessageId: replyId, + ) + : null), + }; + } catch (error) { + return null; + } } static final provider = AsyncNotifierProvider.family