From 881c76359b61b9c042f08dfd335f43f13e6610ed Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 12 May 2026 20:32:40 -0400 Subject: [PATCH] custom link previews --- lib/controllers/url_preview_controller.dart | 30 +++------ lib/models/open_graph_data.dart | 17 +++++ .../wrappers/text_message_wrapper.dart | 62 ++++++++++++------- 3 files changed, 66 insertions(+), 43 deletions(-) create mode 100644 lib/models/open_graph_data.dart diff --git a/lib/controllers/url_preview_controller.dart b/lib/controllers/url_preview_controller.dart index 08770c6..dab3d97 100644 --- a/lib/controllers/url_preview_controller.dart +++ b/lib/controllers/url_preview_controller.dart @@ -4,13 +4,14 @@ import "package:http/http.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/header_controller.dart"; import "package:nexus/helpers/extensions/mxc_to_https.dart"; +import "package:nexus/models/open_graph_data.dart"; -class UrlPreviewController extends AsyncNotifier { +class UrlPreviewController extends AsyncNotifier { final String link; UrlPreviewController(this.link); @override - Future build() async { + Future build() async { final homeserver = ref.watch(ClientStateController.provider)?.homeserverUrl; if (homeserver != null && !link.contains("matrix.to")) { @@ -23,28 +24,15 @@ class UrlPreviewController extends AsyncNotifier { ); if (response.statusCode == 200) { - final decodedValue = json.decode(response.body); - final mxc = decodedValue["og:image"]; + final decodedValue = json.decode(response.body) as Map?; + if (decodedValue?.isNotEmpty == true) return null; + + final mxc = decodedValue!["og:image"]; final image = mxc == null ? null : Uri.tryParse(mxc)?.mxcToHttps(homeserver); - return LinkPreviewData( - link: link, - title: decodedValue["og:title"], - description: decodedValue["og:description"], - image: image == null - ? null - : ImagePreviewData( - url: image.toString(), - width: - (decodedValue["og:image:width"] as int?)?.toDouble() ?? - 0, - height: - (decodedValue["og:image:height"] as int?)?.toDouble() ?? - 0, - ), - ); + return OpenGraphData.fromJson({...decodedValue, "og:image": image}); } } } @@ -53,7 +41,7 @@ class UrlPreviewController extends AsyncNotifier { } static final provider = AsyncNotifierProvider.autoDispose - .family( + .family( UrlPreviewController.new, ); } diff --git a/lib/models/open_graph_data.dart b/lib/models/open_graph_data.dart new file mode 100644 index 0000000..4076edd --- /dev/null +++ b/lib/models/open_graph_data.dart @@ -0,0 +1,17 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "open_graph_data.freezed.dart"; +part "open_graph_data.g.dart"; + +@freezed +abstract class OpenGraphData with _$OpenGraphData { + const factory OpenGraphData({ + @JsonKey(name: "og:title") required String? title, + @JsonKey(name: "og:description") required String? description, + @JsonKey(name: "og:image") required String? imageUrl, + @JsonKey(name: "og:image:width") required double? width, + @JsonKey(name: "og:image:height") required double? height, + }) = _OpenGraphData; + + factory OpenGraphData.fromJson(Map json) => + _$OpenGraphDataFromJson(json); +} diff --git a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart index a3d0192..2870d23 100644 --- a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart @@ -1,6 +1,5 @@ import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; -import "package:flutter_link_previewer/flutter_link_previewer.dart"; import "package:flutter_linkify/flutter_linkify.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/cross_cache_controller.dart"; @@ -109,30 +108,49 @@ class TextMessageWrapper extends ConsumerWidget { loading: SizedBox.shrink, data: (preview) => preview == null ? SizedBox.shrink() - : LinkPreview( - onTap: (url) => ref + : InkWell( + onTap: () => ref .watch(LaunchHelper.provider) - .launchUrl(Uri.parse(url)), - imageBuilder: (url) => Image( - image: CachedNetworkImage( - url, - ref.watch(CrossCacheController.provider), - headers: ref.headers, + .launchUrl(Uri.parse(link)), + child: Card( + child: Column( + children: [ + if (preview.title != null) + Text( + preview.title!, + style: theme.textTheme.labelLarge, + ), + if (preview.description != null) + Text(preview.description!), + if (preview.imageUrl != null) + Image( + errorBuilder: (_, _, _) => + SizedBox.shrink(), + width: preview.width, + height: preview.height, + image: CachedNetworkImage( + preview.imageUrl!, + ref.watch( + CrossCacheController.provider, + ), + headers: ref.headers, + ), + fit: BoxFit.cover, + ), + ], ), - fit: BoxFit.cover, - errorBuilder: (_, _, _) => SizedBox.shrink(), + // text: link, + // backgroundColor: isSentByMe + // ? colorScheme.inversePrimary + // : colorScheme.surfaceContainerLow, + // outsidePadding: EdgeInsets.only(top: 4), + // insidePadding: EdgeInsets.symmetric( + // vertical: 8, + // horizontal: 16, + // ), + // linkPreviewData: preview, + // onLinkPreviewDataFetched: (_) => null, ), - text: link, - backgroundColor: isSentByMe - ? colorScheme.inversePrimary - : colorScheme.surfaceContainerLow, - outsidePadding: EdgeInsets.only(top: 4), - insidePadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - linkPreviewData: preview, - onLinkPreviewDataFetched: (_) => null, ), ), ?extra,