From c6e64a3a5f200c0f222b56d19e671c2b602a5342 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 13:17:27 -0800 Subject: [PATCH 1/7] Allow Tor Caching + Our Own Linkify --- lib/l10n/intl_de.arb | 4 +- lib/l10n/intl_en.arb | 4 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_fr.arb | 4 +- lib/l10n/intl_it.arb | 4 +- lib/l10n/intl_pl.arb | 4 +- lib/l10n/intl_pt.arb | 4 +- lib/l10n/intl_ru.arb | 4 +- lib/settings.dart | 16 + lib/third_party/linkify/flutter_linkify.dart | 380 +++++++++++++++++++ lib/third_party/linkify/linkify.dart | 128 +++++++ lib/third_party/linkify/uri.dart | 127 +++++++ lib/views/torstatusview.dart | 12 + lib/widgets/messagebubble.dart | 4 +- pubspec.lock | 14 - pubspec.yaml | 1 - 16 files changed, 690 insertions(+), 24 deletions(-) create mode 100644 lib/third_party/linkify/flutter_linkify.dart create mode 100644 lib/third_party/linkify/linkify.dart create mode 100644 lib/third_party/linkify/uri.dart diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 1d4bcc7a..ff53b0ac 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1,6 +1,8 @@ { "@@locale": "de", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index b38690f5..0e1cb975 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1,6 +1,8 @@ { "@@locale": "en", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 403fc323..81fd56f3 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1,6 +1,8 @@ { "@@locale": "es", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index dc576196..82f8c123 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,8 @@ { "@@locale": "fr", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 43902cb0..2a69d62d 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1,6 +1,8 @@ { "@@locale": "it", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index f66ea23e..882e91de 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -1,6 +1,8 @@ { "@@locale": "pl", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 95ac2390..fa60f581 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -1,6 +1,8 @@ { "@@locale": "pt", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 0a1f3102..4ea0b04e 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -1,6 +1,8 @@ { "@@locale": "ru", - "@@last_modified": "2022-01-17T21:20:54+01:00", + "@@last_modified": "2022-01-18T00:38:14+01:00", + "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", + "torSettingsEnableCache": "Cache Tor Consensus", "labelTorNetwork": "Tor Network", "descriptionACNCircuitInfo": "In depth information about the path that the anonymous communication network is using to connect to this conversation.", "labelACNCircuitInfo": "ACN Circuit Info", diff --git a/lib/settings.dart b/lib/settings.dart index 343a374d..621018c2 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -45,6 +45,10 @@ class Settings extends ChangeNotifier { int _socksPort = -1; int _controlPort = -1; String _customTorAuth = ""; + bool _useTorCache = false; + String _torCacheDir = ""; + + String get torCacheDir => _torCacheDir; void setTheme(String themeId, String mode) { theme = getTheme(themeId, mode); @@ -99,6 +103,8 @@ class Settings extends ChangeNotifier { _customTorConfig = settings["CustomTorrc"] ?? ""; _socksPort = settings["CustomSocksPort"] ?? -1; _controlPort = settings["CustomControlPort"] ?? -1; + _useTorCache = settings["UseTorCache"] ?? false; + _torCacheDir = settings["TorCacheDir"] ?? ""; // Push the experimental settings to Consumers of Settings notifyListeners(); @@ -252,6 +258,12 @@ class Settings extends ChangeNotifier { notifyListeners(); } + bool get useTorCache => _useTorCache; + set useTorCache(bool useTorCache) { + _useTorCache = useTorCache; + notifyListeners(); + } + // Settings / Gettings for setting the custom tor config.. String get torConfig => _customTorConfig; set torConfig(String torConfig) { @@ -304,6 +316,10 @@ class Settings extends ChangeNotifier { "CustomSocksPort": _socksPort, "CustomControlPort": _controlPort, "CustomAuth": _customTorAuth, + "UseTorCache": _useTorCache, + "TorCacheDir": _torCacheDir }; } } + + diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart new file mode 100644 index 00000000..0db37816 --- /dev/null +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -0,0 +1,380 @@ +// +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ and +// subsequently modified... +// Original License for this code: +// MIT License +// Copyright (c) 2020 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +import 'linkify.dart'; + + + +/// Callback clicked link +typedef LinkCallback = void Function(LinkableElement link); + +/// Turns URLs into links +class Linkify extends StatelessWidget { + /// Text to be linkified + final String text; + + /// Linkifiers to be used for linkify + final List linkifiers; + + /// Callback for tapping a link + final LinkCallback? onOpen; + + /// linkify's options. + final LinkifyOptions options; + + // TextSpan + + /// Style for non-link text + final TextStyle? style; + + /// Style of link text + final TextStyle? linkStyle; + + // Text.rich + + /// How the text should be aligned horizontally. + final TextAlign textAlign; + + /// Text direction of the text + final TextDirection? textDirection; + + /// The maximum number of lines for the text to span, wrapping if necessary + final int? maxLines; + + /// How visual overflow should be handled. + final TextOverflow overflow; + + /// The number of font pixels for each logical pixel + final double textScaleFactor; + + /// Whether the text should break at soft line breaks. + final bool softWrap; + + /// The strut style used for the vertical layout + final StrutStyle? strutStyle; + + /// Used to select a font when the same Unicode character can + /// be rendered differently, depending on the locale + final Locale? locale; + + /// Defines how to measure the width of the rendered text. + final TextWidthBasis textWidthBasis; + + /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line. + final TextHeightBehavior? textHeightBehavior; + + const Linkify({ + Key? key, + required this.text, + this.linkifiers = defaultLinkifiers, + this.onOpen, + this.options = const LinkifyOptions(), + // TextSpan + this.style, + this.linkStyle, + // RichText + this.textAlign = TextAlign.start, + this.textDirection, + this.maxLines, + this.overflow = TextOverflow.clip, + this.textScaleFactor = 1.0, + this.softWrap = true, + this.strutStyle, + this.locale, + this.textWidthBasis = TextWidthBasis.parent, + this.textHeightBehavior, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final elements = linkify( + text, + options: options, + linkifiers: linkifiers, + ); + + return Text.rich( + buildTextSpan( + elements, + style: Theme.of(context).textTheme.bodyText2?.merge(style), + onOpen: onOpen, + useMouseRegion: true, + linkStyle: Theme.of(context) + .textTheme + .bodyText2 + ?.merge(style) + .copyWith( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) + .merge(linkStyle), + ), + textAlign: textAlign, + textDirection: textDirection, + maxLines: maxLines, + overflow: overflow, + textScaleFactor: textScaleFactor, + softWrap: softWrap, + strutStyle: strutStyle, + locale: locale, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + ); + } +} + +/// Turns URLs into links +class SelectableLinkify extends StatelessWidget { + /// Text to be linkified + final String text; + + /// The number of font pixels for each logical pixel + final textScaleFactor; + + /// Linkifiers to be used for linkify + final List linkifiers; + + /// Callback for tapping a link + final LinkCallback? onOpen; + + /// linkify's options. + final LinkifyOptions options; + + // TextSpan + + /// Style for non-link text + final TextStyle? style; + + /// Style of link text + final TextStyle? linkStyle; + + // Text.rich + + /// How the text should be aligned horizontally. + final TextAlign? textAlign; + + /// Text direction of the text + final TextDirection? textDirection; + + /// The minimum number of lines to occupy when the content spans fewer lines. + final int? minLines; + + /// The maximum number of lines for the text to span, wrapping if necessary + final int? maxLines; + + /// The strut style used for the vertical layout + final StrutStyle? strutStyle; + + /// Defines how to measure the width of the rendered text. + final TextWidthBasis? textWidthBasis; + + // SelectableText.rich + + /// Defines the focus for this widget. + final FocusNode? focusNode; + + /// Whether to show cursor + final bool showCursor; + + /// Whether this text field should focus itself if nothing else is already focused. + final bool autofocus; + + /// Configuration of toolbar options + final ToolbarOptions? toolbarOptions; + + /// How thick the cursor will be + final double cursorWidth; + + /// How rounded the corners of the cursor should be + final Radius? cursorRadius; + + /// The color to use when painting the cursor + final Color? cursorColor; + + /// Determines the way that drag start behavior is handled + final DragStartBehavior dragStartBehavior; + + /// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu, + /// and tapping will move the text caret + final bool enableInteractiveSelection; + + /// Called when the user taps on this selectable text (not link) + final GestureTapCallback? onTap; + + final ScrollPhysics? scrollPhysics; + + /// Defines how the paragraph will apply TextStyle.height to the ascent of the first line and descent of the last line. + final TextHeightBehavior? textHeightBehavior; + + /// How tall the cursor will be. + final double? cursorHeight; + + /// Optional delegate for building the text selection handles and toolbar. + final TextSelectionControls? selectionControls; + + /// Called when the user changes the selection of text (including the cursor location). + final SelectionChangedCallback? onSelectionChanged; + + const SelectableLinkify({ + Key? key, + required this.text, + this.linkifiers = defaultLinkifiers, + this.onOpen, + this.options = const LinkifyOptions(), + // TextSpan + this.style, + this.linkStyle, + // RichText + this.textAlign, + this.textDirection, + this.minLines, + this.maxLines, + // SelectableText + this.focusNode, + this.textScaleFactor = 1.0, + this.strutStyle, + this.showCursor = false, + this.autofocus = false, + this.toolbarOptions, + this.cursorWidth = 2.0, + this.cursorRadius, + this.cursorColor, + this.dragStartBehavior = DragStartBehavior.start, + this.enableInteractiveSelection = true, + this.onTap, + this.scrollPhysics, + this.textWidthBasis, + this.textHeightBehavior, + this.cursorHeight, + this.selectionControls, + this.onSelectionChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final elements = linkify( + text, + options: options, + linkifiers: linkifiers, + ); + + return SelectableText.rich( + buildTextSpan( + elements, + style: Theme.of(context).textTheme.bodyText2?.merge(style), + onOpen: onOpen, + linkStyle: Theme.of(context) + .textTheme + .bodyText2 + ?.merge(style) + .copyWith( + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) + .merge(linkStyle), + ), + textAlign: textAlign, + textDirection: textDirection, + minLines: minLines, + maxLines: maxLines, + focusNode: focusNode, + strutStyle: strutStyle, + showCursor: showCursor, + textScaleFactor: textScaleFactor, + autofocus: autofocus, + toolbarOptions: toolbarOptions, + cursorWidth: cursorWidth, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + dragStartBehavior: dragStartBehavior, + enableInteractiveSelection: enableInteractiveSelection, + onTap: onTap, + scrollPhysics: scrollPhysics, + textWidthBasis: textWidthBasis, + textHeightBehavior: textHeightBehavior, + cursorHeight: cursorHeight, + selectionControls: selectionControls, + onSelectionChanged: onSelectionChanged, + ); + } +} + +class LinkableSpan extends WidgetSpan { + LinkableSpan({ + required MouseCursor mouseCursor, + required InlineSpan inlineSpan, + }) : super( + child: MouseRegion( + cursor: mouseCursor, + child: Text.rich( + inlineSpan, + ), + ), + ); +} + +/// Raw TextSpan builder for more control on the RichText +TextSpan buildTextSpan( + List elements, { + TextStyle? style, + TextStyle? linkStyle, + LinkCallback? onOpen, + bool useMouseRegion = false, + }) { + return TextSpan( + children: elements.map( + (element) { + if (element is LinkableElement) { + if (useMouseRegion) { + return LinkableSpan( + mouseCursor: SystemMouseCursors.click, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ), + ); + } else { + return TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ); + } + } else { + return TextSpan( + text: element.text, + style: style, + ); + } + }, + ).toList(), + ); +} diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart new file mode 100644 index 00000000..ea6f027a --- /dev/null +++ b/lib/third_party/linkify/linkify.dart @@ -0,0 +1,128 @@ +// Originally from linkify +// MIT License +// +// Copyright (c) 2019 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:cwtch/third_party/linkify/uri.dart'; + +abstract class LinkifyElement { + final String text; + + LinkifyElement(this.text); + + @override + bool operator ==(other) => equals(other); + + bool equals(other) => other is LinkifyElement && other.text == text; +} + +class LinkableElement extends LinkifyElement { + final String url; + + LinkableElement(String? text, this.url) : super(text ?? url); + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => + other is LinkableElement && super.equals(other) && other.url == url; +} + +/// Represents an element containing text +class TextElement extends LinkifyElement { + TextElement(String text) : super(text); + + @override + String toString() { + return "TextElement: '$text'"; + } + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => other is TextElement && super.equals(other); +} + +abstract class Linkifier { + const Linkifier(); + + List parse( + List elements, LinkifyOptions options); +} + +class LinkifyOptions { + /// Removes http/https from shown URLs. + final bool humanize; + + /// Removes www. from shown URLs. + final bool removeWww; + + /// Enables loose URL parsing (any string with "." is a URL). + final bool looseUrl; + + /// When used with [looseUrl], default to `https` instead of `http`. + final bool defaultToHttps; + + /// Excludes `.` at end of URLs. + final bool excludeLastPeriod; + + const LinkifyOptions({ + this.humanize = true, + this.removeWww = false, + this.looseUrl = false, + this.defaultToHttps = false, + this.excludeLastPeriod = true, + }); +} + +const _urlLinkifier = UrlLinkifier(); +const defaultLinkifiers = [_urlLinkifier]; + +/// Turns [text] into a list of [LinkifyElement] +/// +/// Use [humanize] to remove http/https from the start of the URL shown. +/// Will default to `false` (if `null`) +/// +/// Uses [linkTypes] to enable some types of links (URL, email). +/// Will default to all (if `null`). +List linkify( + String text, { + LinkifyOptions options = const LinkifyOptions(), + List linkifiers = defaultLinkifiers, + }) { + var list = [TextElement(text)]; + + if (text.isEmpty) { + return []; + } + + if (linkifiers.isEmpty) { + return list; + } + + linkifiers.forEach((linkifier) { + list = linkifier.parse(list, options); + }); + + return list; +} diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart new file mode 100644 index 00000000..4c432f09 --- /dev/null +++ b/lib/third_party/linkify/uri.dart @@ -0,0 +1,127 @@ +// Originally from linkify +// MIT License +// +// Copyright (c) 2019 Charles-William Crete +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:cwtch/third_party/linkify/linkify.dart'; + +final _urlRegex = RegExp( + r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', + caseSensitive: false, + dotAll: true, +); + +final _looseUrlRegex = RegExp( + r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))', + caseSensitive: false, + dotAll: true, +); + +final _protocolIdentifierRegex = RegExp( + r'^(https?:\/\/)', + caseSensitive: false, +); + +class UrlLinkifier extends Linkifier { + const UrlLinkifier(); + + @override + List parse(elements, options) { + final list = []; + + elements.forEach((element) { + if (element is TextElement) { + var match = options.looseUrl + ? _looseUrlRegex.firstMatch(element.text) + : _urlRegex.firstMatch(element.text); + + if (match == null) { + list.add(element); + } else { + final text = element.text.replaceFirst(match.group(0)!, ''); + + if (match.group(1)?.isNotEmpty == true) { + list.add(TextElement(match.group(1)!)); + } + + if (match.group(2)?.isNotEmpty == true) { + var originalUrl = match.group(2)!; + String? end; + + if ((options.excludeLastPeriod) && + originalUrl[originalUrl.length - 1] == ".") { + end = "."; + originalUrl = originalUrl.substring(0, originalUrl.length - 1); + } + + var url = originalUrl; + + // We do not, ever, change the original text of a message. + if (options.defaultToHttps) { + url = url.replaceFirst('http://', 'https://'); + } + + // These options are intended for the human-readable portion of + // the URI + if (options.humanize) { + originalUrl = originalUrl.replaceFirst(RegExp(r'https?://'), ''); + } + + if (options.removeWww) { + originalUrl = originalUrl.replaceFirst(RegExp(r'www\.'), ''); + } + + list.add(UrlElement(originalUrl, url)); + + + if (end != null) { + list.add(TextElement(end)); + } + } + + if (text.isNotEmpty) { + list.addAll(parse([TextElement(text)], options)); + } + } + } else { + list.add(element); + } + }); + + return list; + } +} + +/// Represents an element containing a link +class UrlElement extends LinkableElement { + UrlElement(String url, [String? text]) : super(text, url); + + @override + String toString() { + return "LinkElement: '$url' ($text)"; + } + + @override + bool operator ==(other) => equals(other); + + @override + bool equals(other) => other is UrlElement && super.equals(other); +} diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 982bfbaa..ed2ac999 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -76,6 +76,18 @@ class _TorStatusView extends State { subtitle: SelectableText(torStatus.version), leading: Icon(CwtchIcons.info_24px, color: settings.current().mainTextColor), ), + SwitchListTile( + title: Text(AppLocalizations.of(context)!.torSettingsEnableCache), + subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledCacheDescription), + value: settings.useTorCache, + onChanged: (bool value) { + settings.useTorCache = value; + saveSettings(context); + }, + activeTrackColor: settings.theme.defaultButtonColor, + inactiveTrackColor: settings.theme.defaultButtonDisabledColor, + secondary: Icon(Icons.cached, color: settings.current().mainTextColor), + ), SwitchListTile( title: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvanced), subtitle: Text(AppLocalizations.of(context)!.torSettingsEnabledAdvancedDescription), diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 469a1b5a..f1a26eee 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,6 +1,9 @@ import 'dart:io'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; +import 'package:cwtch/third_party/linkify/linkify.dart'; +import 'package:cwtch/third_party/linkify/uri.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +11,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../model.dart'; import 'package:intl/intl.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import '../settings.dart'; diff --git a/pubspec.lock b/pubspec.lock index 3c1db18c..a967a6be 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -125,13 +125,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - url: "https://pub.dartlang.org" - source: hosted - version: "5.0.2" flutter_localizations: dependency: "direct main" description: flutter @@ -196,13 +189,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3" - linkify: - dependency: transitive - description: - name: linkify - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c93ac51..03d6d590 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 file_picker: ^4.0.1 file_picker_desktop: ^1.1.0 - flutter_linkify: ^5.0.2 url_launcher: ^6.0.12 dev_dependencies: From b3f06d6765dfdb276d2a8db45491424bedb7e353 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 13:47:47 -0800 Subject: [PATCH 2/7] Update lcg --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index c1538941..b8bbdfcc 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-01-17-17-19-v1.5.4-5-g4cf95d6 \ No newline at end of file +2022-01-18-16-33-v1.5.4-8-g2aea700 \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index ed75fff0..2ea2c4de 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-01-17-22-19-v1.5.4-5-g4cf95d6 \ No newline at end of file +2022-01-18-21-29-v1.5.4-8-g2aea700 \ No newline at end of file From cd1bf07fba3bbf9f4916f2464ce695989fd8b62c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:32:45 -0800 Subject: [PATCH 3/7] Responding to @errorinn PR Comments --- lib/settings.dart | 2 - lib/third_party/linkify/flutter_linkify.dart | 48 ++++++++++---------- lib/third_party/linkify/linkify.dart | 26 ++++------- lib/third_party/linkify/uri.dart | 29 +++--------- lib/widgets/messagebubble.dart | 4 +- 5 files changed, 41 insertions(+), 68 deletions(-) diff --git a/lib/settings.dart b/lib/settings.dart index 621018c2..8bfaa3f9 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -321,5 +321,3 @@ class Settings extends ChangeNotifier { }; } } - - diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 0db37816..4e702841 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -1,6 +1,7 @@ +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ +// +// Now uses local `linkify` // -// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ and -// subsequently modified... // Original License for this code: // MIT License // Copyright (c) 2020 Charles-William Crete @@ -23,14 +24,13 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'linkify.dart'; - +export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier; /// Callback clicked link typedef LinkCallback = void Function(LinkableElement link); @@ -131,9 +131,9 @@ class Linkify extends StatelessWidget { .bodyText2 ?.merge(style) .copyWith( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ) + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) .merge(linkStyle), ), textAlign: textAlign, @@ -295,9 +295,9 @@ class SelectableLinkify extends StatelessWidget { .bodyText2 ?.merge(style) .copyWith( - color: Colors.blueAccent, - decoration: TextDecoration.underline, - ) + color: Colors.blueAccent, + decoration: TextDecoration.underline, + ) .merge(linkStyle), ), textAlign: textAlign, @@ -331,26 +331,26 @@ class LinkableSpan extends WidgetSpan { required MouseCursor mouseCursor, required InlineSpan inlineSpan, }) : super( - child: MouseRegion( - cursor: mouseCursor, - child: Text.rich( - inlineSpan, - ), - ), - ); + child: MouseRegion( + cursor: mouseCursor, + child: Text.rich( + inlineSpan, + ), + ), + ); } /// Raw TextSpan builder for more control on the RichText TextSpan buildTextSpan( - List elements, { - TextStyle? style, - TextStyle? linkStyle, - LinkCallback? onOpen, - bool useMouseRegion = false, - }) { + List elements, { + TextStyle? style, + TextStyle? linkStyle, + LinkCallback? onOpen, + bool useMouseRegion = false, +}) { return TextSpan( children: elements.map( - (element) { + (element) { if (element is LinkableElement) { if (useMouseRegion) { return LinkableSpan( diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index ea6f027a..381babb6 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -1,4 +1,6 @@ -// Originally from linkify +// Originally from linkify https://github.com/Cretezy/linkify/blob/master/lib/linkify.dart +// Removed options `removeWWW` and `humanize` +// // MIT License // // Copyright (c) 2019 Charles-William Crete @@ -43,8 +45,7 @@ class LinkableElement extends LinkifyElement { bool operator ==(other) => equals(other); @override - bool equals(other) => - other is LinkableElement && super.equals(other) && other.url == url; + bool equals(other) => other is LinkableElement && super.equals(other) && other.url == url; } /// Represents an element containing text @@ -66,17 +67,10 @@ class TextElement extends LinkifyElement { abstract class Linkifier { const Linkifier(); - List parse( - List elements, LinkifyOptions options); + List parse(List elements, LinkifyOptions options); } class LinkifyOptions { - /// Removes http/https from shown URLs. - final bool humanize; - - /// Removes www. from shown URLs. - final bool removeWww; - /// Enables loose URL parsing (any string with "." is a URL). final bool looseUrl; @@ -87,8 +81,6 @@ class LinkifyOptions { final bool excludeLastPeriod; const LinkifyOptions({ - this.humanize = true, - this.removeWww = false, this.looseUrl = false, this.defaultToHttps = false, this.excludeLastPeriod = true, @@ -106,10 +98,10 @@ const defaultLinkifiers = [_urlLinkifier]; /// Uses [linkTypes] to enable some types of links (URL, email). /// Will default to all (if `null`). List linkify( - String text, { - LinkifyOptions options = const LinkifyOptions(), - List linkifiers = defaultLinkifiers, - }) { + String text, { + LinkifyOptions options = const LinkifyOptions(), + List linkifiers = defaultLinkifiers, +}) { var list = [TextElement(text)]; if (text.isEmpty) { diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 4c432f09..7cca8072 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -1,4 +1,8 @@ -// Originally from linkify +// Originally from linkify: https://github.com/Cretezy/linkify/blob/master/lib/src/url.dart +// +// Removed handling of `removeWWW` and `humanize`. +// Removed auto-appending of `http(s)://` to the readable url +// // MIT License // // Copyright (c) 2019 Charles-William Crete @@ -49,9 +53,7 @@ class UrlLinkifier extends Linkifier { elements.forEach((element) { if (element is TextElement) { - var match = options.looseUrl - ? _looseUrlRegex.firstMatch(element.text) - : _urlRegex.firstMatch(element.text); + var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text); if (match == null) { list.add(element); @@ -66,32 +68,15 @@ class UrlLinkifier extends Linkifier { var originalUrl = match.group(2)!; String? end; - if ((options.excludeLastPeriod) && - originalUrl[originalUrl.length - 1] == ".") { + if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") { end = "."; originalUrl = originalUrl.substring(0, originalUrl.length - 1); } var url = originalUrl; - // We do not, ever, change the original text of a message. - if (options.defaultToHttps) { - url = url.replaceFirst('http://', 'https://'); - } - - // These options are intended for the human-readable portion of - // the URI - if (options.humanize) { - originalUrl = originalUrl.replaceFirst(RegExp(r'https?://'), ''); - } - - if (options.removeWww) { - originalUrl = originalUrl.replaceFirst(RegExp(r'www\.'), ''); - } - list.add(UrlElement(originalUrl, url)); - if (end != null) { list.add(TextElement(end)); } diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index f1a26eee..948351cd 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -2,8 +2,6 @@ import 'dart:io'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; -import 'package:cwtch/third_party/linkify/linkify.dart'; -import 'package:cwtch/third_party/linkify/uri.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -69,7 +67,7 @@ class MessageBubbleState extends State { wdgMessage = SelectableLinkify( text: widget.content + '\u202F', // TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler? - options: LinkifyOptions(humanize: false, removeWww: false, looseUrl: true, defaultToHttps: true), + options: LinkifyOptions(looseUrl: true, defaultToHttps: true), linkifiers: [UrlLinkifier()], onOpen: (link) { _modalOpenLink(context, link); From 303b70d75162aad2dc799ab0094df1409f91703f Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:43:49 -0800 Subject: [PATCH 4/7] Fixup displayed link + add linkify to licenses.dart --- lib/licenses.dart | 23 ++++++++++++++++++++ lib/third_party/linkify/flutter_linkify.dart | 2 +- lib/third_party/linkify/linkify.dart | 5 ++++- lib/third_party/linkify/uri.dart | 10 +++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/licenses.dart b/lib/licenses.dart index a665eed0..cc4194c8 100644 --- a/lib/licenses.dart +++ b/lib/licenses.dart @@ -116,4 +116,27 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)"); + + yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], + '''MIT License + +Copyright (c) 2019/2020 Charles-William Crete + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.'''); } diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 4e702841..28638e77 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -30,7 +30,7 @@ import 'package:flutter/rendering.dart'; import 'linkify.dart'; -export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier; +export 'linkify.dart' show LinkifyElement, LinkifyOptions, LinkableElement, TextElement, Linkifier, UrlElement, UrlLinkifier; /// Callback clicked link typedef LinkCallback = void Function(LinkableElement link); diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index 381babb6..9bde6bdd 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -23,7 +23,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:cwtch/third_party/linkify/uri.dart'; + +import 'uri.dart'; +export 'uri.dart' show UrlLinkifier, UrlElement; + abstract class LinkifyElement { final String text; diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 7cca8072..8d0e2874 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -25,7 +25,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:cwtch/third_party/linkify/linkify.dart'; +import 'linkify.dart'; final _urlRegex = RegExp( r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', @@ -75,7 +75,13 @@ class UrlLinkifier extends Linkifier { var url = originalUrl; - list.add(UrlElement(originalUrl, url)); + // If protocol has not been specified then append a protocol + // to the start of the URL so that it can be opened... + if (!url.startsWith("https://") && !url.startsWith("http://")) { + url = "https://"+url; + } + + list.add(UrlElement(url, originalUrl)); if (end != null) { list.add(TextElement(end)); From da3234e3e4926529961ee6f13e0aad7b3674e08d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:44:19 -0800 Subject: [PATCH 5/7] Formatting --- lib/licenses.dart | 3 +-- lib/third_party/linkify/linkify.dart | 2 -- lib/third_party/linkify/uri.dart | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/licenses.dart b/lib/licenses.dart index cc4194c8..057f5c80 100644 --- a/lib/licenses.dart +++ b/lib/licenses.dart @@ -117,8 +117,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'''); yield LicenseEntryWithLineBreaks(["flaticons"], "Icons made by Freepik (https://www.freepik.com) from Flaticon (www.flaticon.com)"); - yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], - '''MIT License + yield LicenseEntryWithLineBreaks(["flutter_linkify", "linkify"], '''MIT License Copyright (c) 2019/2020 Charles-William Crete diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index 9bde6bdd..d4355313 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -23,11 +23,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. - import 'uri.dart'; export 'uri.dart' show UrlLinkifier, UrlElement; - abstract class LinkifyElement { final String text; diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 8d0e2874..4220794c 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -78,7 +78,7 @@ class UrlLinkifier extends Linkifier { // If protocol has not been specified then append a protocol // to the start of the URL so that it can be opened... if (!url.startsWith("https://") && !url.startsWith("http://")) { - url = "https://"+url; + url = "https://" + url; } list.add(UrlElement(url, originalUrl)); From 1700306c787d3e4298fa91f67644f843da981ec9 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 14:48:18 -0800 Subject: [PATCH 6/7] Link to specific commit hashes --- lib/third_party/linkify/flutter_linkify.dart | 2 +- lib/third_party/linkify/linkify.dart | 2 +- lib/third_party/linkify/uri.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 28638e77..9be50666 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -1,4 +1,4 @@ -// Code Originally taken from https://github.com/Cretezy/flutter_linkify/ +// Code Originally taken from https://github.com/Cretezy/flutter_linkify/blob/201e147e0b07b7ca5c543da8167d712d81760753/lib/flutter_linkify.dart // // Now uses local `linkify` // diff --git a/lib/third_party/linkify/linkify.dart b/lib/third_party/linkify/linkify.dart index d4355313..7a7a0248 100644 --- a/lib/third_party/linkify/linkify.dart +++ b/lib/third_party/linkify/linkify.dart @@ -1,4 +1,4 @@ -// Originally from linkify https://github.com/Cretezy/linkify/blob/master/lib/linkify.dart +// Originally from linkify https://github.com/Cretezy/linkify/blob/ba536fa85e7e3a16e580f153616f399458986183/lib/linkify.dart // Removed options `removeWWW` and `humanize` // // MIT License diff --git a/lib/third_party/linkify/uri.dart b/lib/third_party/linkify/uri.dart index 4220794c..9df90bdd 100644 --- a/lib/third_party/linkify/uri.dart +++ b/lib/third_party/linkify/uri.dart @@ -1,4 +1,4 @@ -// Originally from linkify: https://github.com/Cretezy/linkify/blob/master/lib/src/url.dart +// Originally from linkify: https://github.com/Cretezy/linkify/blob/dfb3e43b0e56452bad584ddb0bf9b73d8db0589f/lib/src/url.dart // // Removed handling of `removeWWW` and `humanize`. // Removed auto-appending of `http(s)://` to the readable url From ca44fd798c914c67ac8ac0eeb3eda95316b4f907 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 15:03:54 -0800 Subject: [PATCH 7/7] Show tooltip for links --- lib/third_party/linkify/flutter_linkify.dart | 45 ++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 9be50666..acf82588 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -353,20 +353,24 @@ TextSpan buildTextSpan( (element) { if (element is LinkableElement) { if (useMouseRegion) { - return LinkableSpan( - mouseCursor: SystemMouseCursors.click, - inlineSpan: TextSpan( - text: element.text, - style: linkStyle, - recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, - ), - ); + return TooltipSpan( + message: element.url, + inlineSpan: LinkableSpan( + mouseCursor: SystemMouseCursors.click, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ), + )); } else { - return TextSpan( - text: element.text, - style: linkStyle, - recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, - ); + return TooltipSpan( + message: element.url, + inlineSpan: TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + )); } } else { return TextSpan( @@ -378,3 +382,18 @@ TextSpan buildTextSpan( ).toList(), ); } + +// Show a tooltip over an inlined element in a Rich Text widget. +class TooltipSpan extends WidgetSpan { + TooltipSpan({ + required String message, + required InlineSpan inlineSpan, + }) : super( + child: Tooltip( + message: message, + child: Text.rich( + inlineSpan, + ), + ), + ); +}