@@ 0,0 1,58 @@
+import 'dart:io';
+
+import 'package:linkify/linkify.dart' as lnk;
+
+import 'linkify.dart';
+
+const maxPhotoSize = 10 * 1024 * 1024;
+
+class LinkPreviewer {
+ final HttpClient client = HttpClient();
+
+ void dispose() {
+ client.close();
+ }
+
+ Future<PhotoPreview?> previewUrl(Uri url) async {
+ if (url.scheme != 'https') {
+ return null;
+ }
+
+ var req = await client.headUrl(url);
+ var resp = await req.close();
+ if (resp.statusCode ~/ 100 != 2) {
+ throw Exception('HTTP error fetching $url: ${resp.statusCode}');
+ }
+
+ if (resp.headers.contentType?.primaryType != 'image') {
+ return null;
+ }
+ if (resp.headers.contentLength > maxPhotoSize) {
+ return null;
+ }
+
+ return PhotoPreview(url);
+ }
+
+ Future<List<PhotoPreview>> previewText(String text) async {
+ var links = extractLinks(text);
+
+ List<PhotoPreview> previews = [];
+ await Future.wait(links.map((link) async {
+ if (link is lnk.UrlElement) {
+ var preview = await previewUrl(Uri.parse(link.url));
+ if (preview != null) {
+ previews.add(preview);
+ }
+ }
+ }));
+
+ return previews;
+ }
+}
+
+class PhotoPreview {
+ final Uri url;
+
+ PhotoPreview(this.url);
+}
@@ 3,11 3,15 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:linkify/linkify.dart' as lnk;
import 'package:url_launcher/url_launcher.dart';
-TextSpan linkify(String text, { required TextStyle textStyle, required TextStyle linkStyle }) {
- var elements = lnk.linkify(text, options: lnk.LinkifyOptions(
+List<LinkifyElement> extractLinks(String text) {
+ return lnk.linkify(text, options: lnk.LinkifyOptions(
humanize: false,
defaultToHttps: true,
));
+}
+
+TextSpan linkify(String text, { required TextStyle textStyle, required TextStyle linkStyle }) {
+ var elements = extractLinks(text);
return buildTextSpan(
elements,
onOpen: (link) async {
@@ 3,6 3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:provider/provider.dart';
+import 'package:url_launcher/url_launcher.dart';
import '../ansi.dart';
import '../client.dart';
@@ 10,6 11,7 @@ import '../client_controller.dart';
import '../database.dart';
import '../irc.dart';
import '../linkify.dart';
+import '../link_preview.dart';
import '../models.dart';
import '../notification_controller.dart';
import '../prefs.dart';
@@ 636,6 638,8 @@ class _CompactMessageItem extends StatelessWidget {
}
}
+var linkPreviewer = LinkPreviewer();
+
class _MessageItem extends StatelessWidget {
final MessageModel msg;
final MessageModel? prevMsg, nextMsg;
@@ 651,6 655,36 @@ class _MessageItem extends StatelessWidget {
this.onSwipe
}) : super(key: key);
+ Widget _buildLinkPreview(String text) {
+ return FutureBuilder<List<PhotoPreview>>(
+ future: linkPreviewer.previewText(text),
+ builder: (context, snapshot) {
+ var previews = snapshot.data;
+ if (previews == null || previews.isEmpty) {
+ return Container();
+ }
+ var preview = previews.first;
+ return Container(
+ margin: EdgeInsets.all(10),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(10),
+ child: InkWell(
+ onTap: () {
+ launchUrl(preview.url, mode: LaunchMode.externalApplication);
+ },
+ child: Image.network(
+ preview.url.toString(),
+ width: 250,
+ height: 250,
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+
@override
Widget build(BuildContext context) {
var client = context.read<Client>();
@@ 717,6 751,7 @@ class _MessageItem extends StatelessWidget {
var linkStyle = textStyle.apply(decoration: TextDecoration.underline);
List<InlineSpan> content;
+ Widget? linkPreview;
if (isAction) {
// isAction can only ever be true if we have a ctcp
var actionText = stripAnsiFormatting(ctcp!.param ?? '');
@@ 744,6 779,8 @@ class _MessageItem extends StatelessWidget {
if (isFirstInGroup) TextSpan(text: '\n'),
linkify(body, textStyle: textStyle, linkStyle: linkStyle),
];
+
+ linkPreview = _buildLinkPreview(body);
}
Widget inner = RichText(text: TextSpan(
@@ 829,6 866,7 @@ class _MessageItem extends StatelessWidget {
margin: EdgeInsets.only(left: margin, right: margin, top: marginTop, bottom: marginBottom),
child: decoratedMessage,
),
+ if (linkPreview != null) linkPreview,
]);
}
}