dont fail to parse on invalid reply

This commit is contained in:
Henry Hiles 2026-03-18 22:06:11 -04:00
commit 4087d6ca11
No known key found for this signature in database

View file

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