new linkify handling
Should fix mentions in non-HTML messages
This commit is contained in:
parent
bcb0c57015
commit
40f5d0f459
7 changed files with 123 additions and 42 deletions
45
lib/helpers/extensions/linkify_html.dart
Normal file
45
lib/helpers/extensions/linkify_html.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import "package:html/dom.dart";
|
||||
import "package:html/parser.dart";
|
||||
import "package:linkify/linkify.dart";
|
||||
import "package:nexus/helpers/matrix_linkifier.dart";
|
||||
|
||||
extension LinkifyHtml on String {
|
||||
/// Wraps bare links in this HTML's text in `<a>` tags.
|
||||
///
|
||||
/// Only text nodes are touched, so attribute values are left alone, and
|
||||
/// text already inside a link or code is skipped.
|
||||
String get linkifiedHtml {
|
||||
final fragment = parseFragment(this);
|
||||
_linkifyNode(fragment);
|
||||
return fragment.outerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
void _linkifyNode(Node node) {
|
||||
if (node is Element && const {"a", "code", "pre"}.contains(node.localName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final child in node.nodes.toList()) {
|
||||
if (child is! Text) {
|
||||
_linkifyNode(child);
|
||||
continue;
|
||||
}
|
||||
|
||||
final elements = linkifyText(child.data);
|
||||
if (!elements.any((element) => element is LinkableElement)) continue;
|
||||
|
||||
elements
|
||||
.map(
|
||||
(element) => switch (element) {
|
||||
LinkableElement(:final text, :final url) =>
|
||||
Element.tag("a")
|
||||
..attributes["href"] = url
|
||||
..append(Text(text)),
|
||||
_ => Text(element.text),
|
||||
},
|
||||
)
|
||||
.forEach((replacement) => node.insertBefore(replacement, child));
|
||||
child.remove();
|
||||
}
|
||||
}
|
||||
38
lib/helpers/matrix_linkifier.dart
Normal file
38
lib/helpers/matrix_linkifier.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import "package:linkify/linkify.dart";
|
||||
|
||||
/// Splits [text] into plain text and URL, `matrix:` and email links.
|
||||
List<LinkifyElement> linkifyText(String text) => linkify(
|
||||
text,
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
linkifiers: const [UrlLinkifier(), MatrixLinkifier(), EmailLinkifier()],
|
||||
);
|
||||
|
||||
/// Linkifies `matrix:` URIs, which [UrlLinkifier] only handles for http(s).
|
||||
final class const MatrixLinkifier() extends Linkifier {
|
||||
static final _regex = RegExp(
|
||||
r"\bmatrix:[^\s<>\x22\x27]*[^\s<>\x22\x27.,;:!?)\]}]",
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
@override
|
||||
List<LinkifyElement> parse(
|
||||
List<LinkifyElement> elements,
|
||||
LinkifyOptions options,
|
||||
) => elements
|
||||
.expand(
|
||||
(element) => element is TextElement ? _split(element.text) : [element],
|
||||
)
|
||||
.toList();
|
||||
|
||||
Iterable<LinkifyElement> _split(String text) sync* {
|
||||
var start = 0;
|
||||
for (final match in _regex.allMatches(text)) {
|
||||
if (match.start > start) {
|
||||
yield TextElement(text.substring(start, match.start));
|
||||
}
|
||||
yield UrlElement(match[0]!);
|
||||
start = match.end;
|
||||
}
|
||||
if (start < text.length) yield TextElement(text.substring(start));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,46 @@
|
|||
import "package:flutter/gestures.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_linkify/flutter_linkify.dart";
|
||||
import "package:linkify/linkify.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:nexus/helpers/extensions/get_link_info.dart";
|
||||
import "package:nexus/helpers/launch_helper.dart";
|
||||
import "package:nexus/helpers/matrix_linkifier.dart";
|
||||
import "package:nexus/widgets/html/mention_chip.dart";
|
||||
|
||||
final class const LinkifiedText(
|
||||
final String text, {
|
||||
final String? roomId,
|
||||
final int? maxLines,
|
||||
final TextStyle? style,
|
||||
super.key,
|
||||
}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) =>
|
||||
MaterialUiCompatibilityBridge(
|
||||
child: Linkify(
|
||||
text: text,
|
||||
maxLines: maxLines,
|
||||
Widget build(BuildContext context, WidgetRef ref) => Text.rich(
|
||||
TextSpan(
|
||||
style: style,
|
||||
options: .new(humanize: false),
|
||||
onOpen: (link) =>
|
||||
ref.read(LaunchHelper.provider).launchUrl(.parse(link.url)),
|
||||
linkStyle: .new(color: Theme.of(context).colorScheme.primary),
|
||||
overflow: maxLines == null ? null : .ellipsis,
|
||||
children: linkifyText(text)
|
||||
.map<InlineSpan>(
|
||||
(element) => switch (element) {
|
||||
LinkableElement(:final url) when url.linkInfo != null =>
|
||||
WidgetSpan(
|
||||
alignment: .baseline,
|
||||
baseline: .alphabetic,
|
||||
child: MentionChip(url, roomId),
|
||||
),
|
||||
LinkableElement(:final text, :final url) => TextSpan(
|
||||
text: text,
|
||||
style: .new(color: Theme.of(context).colorScheme.primary),
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () =>
|
||||
ref.read(LaunchHelper.provider).launchUrl(.parse(url)),
|
||||
),
|
||||
_ => TextSpan(text: element.text),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
maxLines: maxLines,
|
||||
overflow: maxLines == null ? null : .ellipsis,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import "package:linkify/linkify.dart";
|
|||
import "package:nexus/controllers/client_state.dart";
|
||||
import "package:nexus/controllers/event.dart";
|
||||
import "package:nexus/helpers/extensions/get_filename.dart";
|
||||
import "package:nexus/helpers/extensions/linkify_html.dart";
|
||||
import "package:nexus/models/content/encrypted.dart";
|
||||
import "package:nexus/models/content/message.dart";
|
||||
import "package:nexus/models/content/sticker.dart";
|
||||
|
|
@ -77,26 +78,11 @@ class const MessageRenderer(
|
|||
? Html(
|
||||
roomId: event.roomId,
|
||||
textStyle: textStyle,
|
||||
formattedBody.replaceAllMapped(
|
||||
RegExp(
|
||||
r"(<a\b[^>]*>.*?<\/a>)|(\bhttps?:\/\/[^\s<]+)",
|
||||
caseSensitive: false,
|
||||
dotAll: true,
|
||||
),
|
||||
(m) {
|
||||
// If it's already an <a> tag, leave it unchanged
|
||||
if (m.group(1) != null) {
|
||||
return m.group(1)!;
|
||||
}
|
||||
|
||||
// Otherwise, wrap the bare URL
|
||||
final url = m.group(2)!;
|
||||
return "<a href=\"$url\">$url</a>";
|
||||
},
|
||||
),
|
||||
formattedBody.linkifiedHtml,
|
||||
)
|
||||
: LinkifiedText(
|
||||
body.isEmpty ? content.filename ?? "" : body,
|
||||
roomId: event.roomId,
|
||||
style: textStyle,
|
||||
maxLines: maxLines,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class const RoomSummarySheet(
|
|||
if (summary.topic != null)
|
||||
ListTile(
|
||||
title: Text("Topic"),
|
||||
subtitle: LinkifiedText(summary.topic!),
|
||||
subtitle: LinkifiedText(summary.topic!, roomId: summary.roomId),
|
||||
leading: Icon(Icons.info),
|
||||
),
|
||||
if (isKnocking)
|
||||
|
|
|
|||
10
pubspec.lock
10
pubspec.lock
|
|
@ -503,14 +503,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.14.4"
|
||||
flutter_linkify:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_linkify
|
||||
sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -672,7 +664,7 @@ packages:
|
|||
source: hosted
|
||||
version: "3.4.3"
|
||||
html:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html
|
||||
sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ dependencies:
|
|||
ffigen: 23.0.0
|
||||
timeago: 3.7.1
|
||||
http: 1.6.0
|
||||
flutter_linkify: 6.0.0
|
||||
linkify: 5.0.0
|
||||
html: 0.15.7
|
||||
flutter_blurhash: 0.9.1
|
||||
super_sliver_list: 0.4.1
|
||||
media_kit: 1.2.6
|
||||
|
|
|
|||
Loading…
Reference in a new issue