diff --git a/README.md b/README.md index a952e0f..070c5db 100644 --- a/README.md +++ b/README.md @@ -67,31 +67,32 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [x] Plain text - [x] Per message profiles - [x] HTML - - [x] Replies - - [x] Viewing - - [ ] Jump to original message - - [x] In loaded timeline - - [ ] Out of loaded timeline - - [x] Edits - - [x] Attachments - - [x] Unencrypted - - [ ] Encrypted - - [x] Blurhashing - - [ ] Downloading attachments - - [x] Opening attachments in their own view - - [ ] Polls: Waiting on https://github.com/SwanFlutter/dynamic_polls/issues/1 - - [x] Mentions - - [x] Users - - [x] Rooms - - [ ] Plain text (not sure if I want to add this or not, I probably won't unless there's interest) - - [x] Matrix URIs - - [x] Matrix.to links - - [ ] Do some fancy fetching to get nice names - - [ ] Make clickable - - [x] Custom emojis/stickers - - [x] History loading - - [x] Backwards - - [ ] Forwards + - [x] URL Previews + - [x] Replies + - [x] Viewing + - [ ] Jump to original message + - [x] In loaded timeline + - [ ] Out of loaded timeline + - [x] Edits + - [x] Attachments + - [x] Unencrypted + - [ ] Encrypted + - [x] Blurhashing + - [ ] Downloading attachments + - [x] Opening attachments in their own view + - [ ] Polls: Waiting on https://github.com/SwanFlutter/dynamic_polls/issues/1 + - [x] Mentions + - [x] Users + - [x] Rooms + - [ ] Plain text (not sure if I want to add this or not, I probably won't unless there's interest) + - [x] Matrix URIs + - [x] Matrix.to links + - [ ] Do some fancy fetching to get nice names + - [ ] Make clickable + - [x] Custom emojis/stickers + - [x] History loading + - [x] Backwards + - [ ] Forwards - [x] Editing - [x] Deleting - [ ] Reactions: Waiting on https://github.com/flyerhq/flutter_chat_ui/pull/838 or me doing a custom impl @@ -116,7 +117,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend. - [ ] Devices - [ ] Viewing devices - [ ] Verifying devices - - [ ] URL preview: Server / Client / None + - [ ] URL preview: Server / Sending Client (Beeper spec) / None - [ ] Account changes - [ ] Display name - [ ] Profile picture diff --git a/lib/controllers/url_preview_controller.dart b/lib/controllers/url_preview_controller.dart new file mode 100644 index 0000000..119a845 --- /dev/null +++ b/lib/controllers/url_preview_controller.dart @@ -0,0 +1,63 @@ +import "dart:convert"; +import "package:flutter_chat_core/flutter_chat_core.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +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"; + +class UrlPreviewController extends AsyncNotifier { + final TextMessage message; + UrlPreviewController(this.message); + + @override + Future build() async { + final homeserver = ref.watch(ClientStateController.provider)?.homeserverUrl; + final link = RegExp( + r'''https?://[^\s"'<>]+''', + ).allMatches(message.text).firstOrNull?.group(0); + + if (homeserver != null && link != null) { + { + final response = await get( + Uri.parse(homeserver) + .resolve("/_matrix/client/v1/media/preview_url") + .replace(queryParameters: {"url": link}), + headers: await ref.watch(HeaderController.provider.future), + ); + + if (response.statusCode == 200) { + final decodedValue = json.decode(response.body); + 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 null; + } + + static final provider = AsyncNotifierProvider.autoDispose + .family( + UrlPreviewController.new, + ); +} diff --git a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart index 63329b9..08e583e 100644 --- a/lib/widgets/chat_page/wrappers/text_message_wrapper.dart +++ b/lib/widgets/chat_page/wrappers/text_message_wrapper.dart @@ -1,11 +1,17 @@ +import "package:cross_cache/cross_cache.dart"; import "package:flutter/material.dart"; import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_link_previewer/flutter_link_previewer.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/cross_cache_controller.dart"; +import "package:nexus/controllers/url_preview_controller.dart"; +import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/widgets/chat_page/html/html.dart"; import "package:nexus/widgets/chat_page/wrappers/message_wrapper.dart"; import "package:nexus/widgets/chat_page/reply_widget.dart"; -class TextMessageWrapper extends StatelessWidget { +class TextMessageWrapper extends ConsumerWidget { final Message message; final String? content; final MessageGroupStatus? groupStatus; @@ -27,7 +33,7 @@ class TextMessageWrapper extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final textMessage = message is TextMessage ? message as TextMessage : null; @@ -80,27 +86,35 @@ class TextMessageWrapper extends StatelessWidget { if (textMessage?.editedAt != null) Text("(edited)", style: theme.textTheme.labelSmall), if (textMessage != null) - LinkPreview( - text: textMessage.text, - backgroundColor: isSentByMe - ? colorScheme.inversePrimary - : colorScheme.surfaceContainerLow, - outsidePadding: EdgeInsets.only(top: 4), - insidePadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - linkPreviewData: message.metadata?["linkPreviewData"], - onLinkPreviewDataFetched: (linkPreviewData) => updateMessage( - message, - message.copyWith( - metadata: { - ...(message.metadata ?? {}), - "linkPreviewData": linkPreviewData, - }, + ref + .watch(UrlPreviewController.provider(textMessage)) + .betterWhen( + loading: SizedBox.shrink, + data: (preview) => preview == null + ? SizedBox.shrink() + : LinkPreview( + imageBuilder: (url) => Image( + image: CachedNetworkImage( + url, + ref.watch(CrossCacheController.provider), + headers: ref.headers, + ), + fit: BoxFit.cover, + errorBuilder: (_, _, _) => SizedBox.shrink(), + ), + text: textMessage.text, + backgroundColor: isSentByMe + ? colorScheme.inversePrimary + : colorScheme.surfaceContainerLow, + outsidePadding: EdgeInsets.only(top: 4), + insidePadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + linkPreviewData: preview, + onLinkPreviewDataFetched: (_) => null, + ), ), - ), - ), if (extra != null) extra!, ], ), diff --git a/pubspec.lock b/pubspec.lock index 1352319..46b8ee6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -655,7 +655,7 @@ packages: source: hosted version: "0.15.6" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index 07baf46..482e809 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: code_assets: ^1.0.0 ffigen: ^20.1.1 timeago: ^3.7.1 + http: ^1.6.0 dev_dependencies: build_runner: ^2.4.11