From c6e64a3a5f200c0f222b56d19e671c2b602a5342 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Tue, 18 Jan 2022 13:17:27 -0800 Subject: [PATCH] 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: