add html support

This commit is contained in:
Henry Hiles 2025-11-14 22:05:35 -05:00
commit 8d3c657ff6
No known key found for this signature in database
6 changed files with 58 additions and 55 deletions

View file

@ -1,12 +1,10 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter/widgets.dart";
import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_chat_core/flutter_chat_core.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:matrix/matrix.dart"; import "package:matrix/matrix.dart";
import "package:nexus/models/full_room.dart"; import "package:nexus/models/full_room.dart";
import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
import "package:html2md/html2md.dart";
extension BetterWhen<T> on AsyncValue<T> { extension BetterWhen<T> on AsyncValue<T> {
Widget betterWhen({ Widget betterWhen({
@ -39,6 +37,7 @@ extension ToMessage on Event {
? relationshipEventId ? relationshipEventId
: null; : null;
final metadata = { final metadata = {
"formatted": formattedText.isEmpty ? body : formattedText,
"eventType": type, "eventType": type,
"displayName": senderFromMemoryOrFallback.displayName, "displayName": senderFromMemoryOrFallback.displayName,
"txnId": transactionId, "txnId": transactionId,
@ -49,34 +48,34 @@ extension ToMessage on Event {
metadata: metadata, metadata: metadata,
id: eventId, id: eventId,
authorId: senderId, authorId: senderId,
text: "~~This message has been redacted.~~", text: "<s>This message has been redacted.</s>",
deletedAt: redactedBecause?.originServerTs, deletedAt: redactedBecause?.originServerTs,
); );
} }
final formatted = convert( final asText =
formattedText.isEmpty ? body : formattedText, Message.text(
ignore: replyId == null ? null : ["mx-reply"], metadata: metadata,
); id: eventId,
authorId: senderId,
final asText = Message.text( text: body,
metadata: metadata, replyToMessageId: replyId,
id: eventId, deliveredAt: originServerTs,
authorId: senderId, )
text: formatted, as TextMessage;
replyToMessageId: replyId,
deliveredAt: originServerTs,
);
if (mustBeText) return asText; if (mustBeText) return asText;
return switch (type) { return switch (type) {
EventTypes.Encrypted => asText.copyWith(
text: "Unable to decrypt message.",
),
EventTypes.Message => switch (messageType) { EventTypes.Message => switch (messageType) {
MessageTypes.Image => Message.image( MessageTypes.Image => Message.image(
metadata: metadata, metadata: metadata,
id: eventId, id: eventId,
authorId: senderId, authorId: senderId,
text: formatted, text: text,
source: (await getAttachmentUri()).toString(), source: (await getAttachmentUri()).toString(),
replyToMessageId: replyId, replyToMessageId: replyId,
deliveredAt: originServerTs, deliveredAt: originServerTs,
@ -85,7 +84,7 @@ extension ToMessage on Event {
metadata: metadata, metadata: metadata,
id: eventId, id: eventId,
authorId: senderId, authorId: senderId,
text: formatted, text: text,
replyToMessageId: replyId, replyToMessageId: replyId,
source: (await getAttachmentUri()).toString(), source: (await getAttachmentUri()).toString(),
deliveredAt: originServerTs, deliveredAt: originServerTs,

View file

@ -6,16 +6,16 @@ class LaunchHelper {
final Ref ref; final Ref ref;
LaunchHelper(this.ref); LaunchHelper(this.ref);
Future<void> launchUrl(Uri url, {bool useWebview = false}) async { Future<bool> launchUrl(Uri url, {bool useWebview = false}) async {
try { try {
await ul.launchUrl( return await ul.launchUrl(
url, url,
mode: useWebview mode: useWebview
? ul.LaunchMode.inAppBrowserView ? ul.LaunchMode.inAppBrowserView
: ul.LaunchMode.externalApplication, : ul.LaunchMode.externalApplication,
); );
} on PlatformException catch (_) { } on PlatformException catch (_) {
// Ignore missing intent handler error return false;
} }
} }

View file

@ -38,14 +38,15 @@ class RoomAppbar extends StatelessWidget implements PreferredSizeWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(room.title, overflow: TextOverflow.ellipsis), Text(room.title, overflow: TextOverflow.ellipsis),
Text( if (room.roomData.topic.isNotEmpty)
room.roomData.topic, Text(
maxLines: 1, room.roomData.topic,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: Theme.of(context).textTheme.labelMedium?.copyWith( overflow: TextOverflow.ellipsis,
color: Theme.of(context).colorScheme.onSurfaceVariant, style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
), ),
),
], ],
), ),
actions: [ actions: [

View file

@ -17,6 +17,7 @@ import "package:nexus/widgets/chat_box.dart";
import "package:nexus/widgets/member_list.dart"; import "package:nexus/widgets/member_list.dart";
import "package:nexus/widgets/room_appbar.dart"; import "package:nexus/widgets/room_appbar.dart";
import "package:nexus/widgets/top_widget.dart"; import "package:nexus/widgets/top_widget.dart";
import "package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart";
class RoomChat extends HookConsumerWidget { class RoomChat extends HookConsumerWidget {
final bool isDesktop; final bool isDesktop;
@ -122,26 +123,24 @@ class RoomChat extends HookConsumerWidget {
required bool isSentByMe, required bool isSentByMe,
MessageGroupStatus? groupStatus, MessageGroupStatus? groupStatus,
}) => FlyerChatTextMessage( }) => FlyerChatTextMessage(
customWidget: HtmlWidget(
message.metadata?["formatted"],
customWidgetBuilder: (element) =>
element.localName == "mx-reply"
? SizedBox.shrink()
: null,
onTapUrl: (url) => ref
.watch(LaunchHelper.provider)
.launchUrl(Uri.parse(url)),
),
topWidget: TopWidget( topWidget: TopWidget(
message, message,
headers: room.roomData.client.headers, headers: room.roomData.client.headers,
groupStatus: groupStatus, groupStatus: groupStatus,
), ),
message: message.copyWith( message: message,
text: message.text.replaceAllMapped(
urlRegex,
(match) =>
"[${match.group(0)}](${match.group(0)})",
),
),
showTime: true, showTime: true,
index: index, index: index,
onLinkTap: (url, _) => ref
.watch(LaunchHelper.provider)
.launchUrl(Uri.parse(url)),
linksDecoration: TextDecoration.underline,
sentLinksColor: Colors.blue,
receivedLinksColor: Colors.blue,
), ),
linkPreviewBuilder: (_, message, isSentByMe) => linkPreviewBuilder: (_, message, isSentByMe) =>
LinkPreview( LinkPreview(

View file

@ -590,6 +590,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_widget_from_html_core:
dependency: "direct main"
description:
name: flutter_widget_from_html_core
sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
flyer_chat_file_message: flyer_chat_file_message:
dependency: "direct main" dependency: "direct main"
description: description:
@ -617,10 +625,11 @@ packages:
flyer_chat_text_message: flyer_chat_text_message:
dependency: "direct main" dependency: "direct main"
description: description:
name: flyer_chat_text_message path: "packages/flyer_chat_text_message"
sha256: dad7a0c29803233ca55cf8318ed9962c657864dc0a464d8cb76469b9e4da07e7 ref: HEAD
url: "https://pub.dev" resolved-ref: dd1b93e6dd4194b3bd934e0d9c27438ba25322cb
source: hosted url: "https://github.com/Henry-Hiles/flutter_chat_ui"
source: git
version: "2.5.2" version: "2.5.2"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
@ -702,14 +711,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.15.6" version: "0.15.6"
html2md:
dependency: "direct main"
description:
name: html2md
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
html_unescape: html_unescape:
dependency: transitive dependency: transitive
description: description:

View file

@ -42,8 +42,11 @@ dependencies:
flutter_chat_core: ^2.0.0 flutter_chat_core: ^2.0.0
flyer_chat_image_message: ^2.2.2 flyer_chat_image_message: ^2.2.2
flyer_chat_system_message: ^2.1.13 flyer_chat_system_message: ^2.1.13
flyer_chat_text_message: ^2.5.2
flyer_chat_file_message: ^2.3.1 flyer_chat_file_message: ^2.3.1
flyer_chat_text_message:
git:
url: https://github.com/Henry-Hiles/flutter_chat_ui
path: packages/flyer_chat_text_message
flutter_chat_ui: flutter_chat_ui:
git: git:
url: https://github.com/Henry-Hiles/flutter_chat_ui url: https://github.com/Henry-Hiles/flutter_chat_ui
@ -53,8 +56,8 @@ dependencies:
sqflite_common_ffi: ^2.3.6 sqflite_common_ffi: ^2.3.6
color_hash: ^1.0.1 color_hash: ^1.0.1
scaled_app: ^2.3.0 scaled_app: ^2.3.0
html2md: ^1.3.2
flutter_vodozemac: ^0.4.1 flutter_vodozemac: ^0.4.1
flutter_widget_from_html_core: ^0.17.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.11 build_runner: ^2.4.11