diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ec51df3e..d562b149 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,6 +30,11 @@
+
+
+
+
+
diff --git a/lib/settings.dart b/lib/settings.dart
index c0955489..8573aa97 100644
--- a/lib/settings.dart
+++ b/lib/settings.dart
@@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
const FileSharingExperiment = "filesharing";
+const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode {
Single,
diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart
index 20695220..e20f2970 100644
--- a/lib/views/globalsettingsview.dart
+++ b/lib/views/globalsettingsview.dart
@@ -204,6 +204,22 @@ class _GlobalSettingsViewState extends State {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()),
),
+ SwitchListTile(
+ title: Text("Enable Clickable Links", style: TextStyle(color: settings.current().mainTextColor())),
+ subtitle: Text("The clickable links experiment allows you to click on URLs shared in messages."),
+ value: settings.isExperimentEnabled(ClickableLinksExperiment),
+ onChanged: (bool value) {
+ if (value) {
+ settings.enableExperiment(ClickableLinksExperiment);
+ } else {
+ settings.disableExperiment(ClickableLinksExperiment);
+ }
+ saveSettings(context);
+ },
+ activeTrackColor: settings.theme.defaultButtonActiveColor(),
+ inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
+ secondary: Icon(Icons.link, color: settings.current().mainTextColor()),
+ ),
],
)),
AboutListTile(
diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart
index 2ba8fe0c..39a8f9a3 100644
--- a/lib/widgets/messagebubble.dart
+++ b/lib/widgets/messagebubble.dart
@@ -3,9 +3,13 @@ import 'dart:io';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+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';
import 'messagebubbledecorations.dart';
@@ -28,6 +32,7 @@ class MessageBubbleState extends State {
var prettyDate = "";
var borderRadiousEh = 15.0;
// var myKey = Provider.of(context).profileOnion + "::" + Provider.of(context).contactHandle + "::" + Provider.of(context).messageIndex.toString();
+ var showClickableLinks = Provider.of(context).isExperimentEnabled(ClickableLinksExperiment);
DateTime messageDate = Provider.of(context).timestamp;
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
@@ -45,16 +50,37 @@ class MessageBubbleState extends State {
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor()));
- var wdgMessage = SelectableText(
- widget.content + '\u202F',
- //key: Key(myKey),
- focusNode: _focus,
- style: TextStyle(
- color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(),
- ),
- textAlign: TextAlign.left,
- textWidthBasis: TextWidthBasis.longestLine,
- );
+ var wdgMessage;
+
+ if (!showClickableLinks) {
+ wdgMessage = SelectableText(
+ widget.content + '\u202F',
+ //key: Key(myKey),
+ focusNode: _focus,
+ style: TextStyle(
+ color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(),
+ ),
+ textAlign: TextAlign.left,
+ textWidthBasis: TextWidthBasis.longestLine,
+ );
+ } else {
+ wdgMessage = SelectableLinkify(
+ text: widget.content + '\u202F',
+ // TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
+ options: LinkifyOptions(humanize: false),
+ linkifiers: [UrlLinkifier()], // TODO: double-check on this (only web links to avoid Android messiness)
+ onOpen: (link) {
+ _modalOpenLink(context, link);
+ },
+ //key: Key(myKey),
+ focusNode: _focus,
+ style: TextStyle(
+ color: fromMe ? Provider.of(context).theme.messageFromMeTextColor() : Provider.of(context).theme.messageFromOtherTextColor(),
+ ),
+ textAlign: TextAlign.left,
+ textWidthBasis: TextWidthBasis.longestLine,
+ );
+ }
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of(context).ackd, errored: Provider.of(context).error, fromMe: fromMe, prettyDate: prettyDate);
@@ -90,4 +116,58 @@ class MessageBubbleState extends State {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
});
}
+
+ void _modalOpenLink(BuildContext ctx, LinkableElement link) {
+ showModalBottomSheet(
+ context: ctx,
+ builder: (BuildContext bcontext) {
+ return Container(
+ // TODO: Ask re: hard-coded height
+ height: 200, // bespoke value courtesy of the [TextField] docs
+ child: Center(
+ child: Padding(
+ padding: EdgeInsets.all(30.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ "Opening this link will launch an application outside of Cwtch and may reveal metadata or otherwise compromise the security of Cwtch. Only open links from people you trust. Are you sure you want to continue?"
+ ),
+ // TODO: Ask about styling preferences (should this be a reusable "inline-button"?)
+ Flex(direction: Axis.horizontal, mainAxisAlignment: MainAxisAlignment.center, children: [
+ Container(
+ margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
+ child: ElevatedButton(
+ child: Text("Copy link", semanticsLabel: "Copy link"),
+ onPressed: () {
+ Clipboard.setData(new ClipboardData(text: link.url));
+
+ // TODO: Ask about desired SnackBar + modal behaviour
+ final snackBar = SnackBar(
+ content: Text(AppLocalizations.of(context)!.copiedClipboardNotification),
+ );
+ ScaffoldMessenger.of(context).showSnackBar(snackBar);
+ },
+ ),
+ ),
+ Container(
+ margin: EdgeInsets.symmetric(vertical: 20, horizontal: 10),
+ child: ElevatedButton(
+ child: Text("Open link", semanticsLabel: "Open link"),
+ onPressed: () async {
+ if (await canLaunch(link.url)) {
+ await launch(link.url);
+ } else {
+ throw 'Could not launch $link';
+ }
+ },
+ ),
+ ),
+ ]),
+ ],
+ )),
+ ));
+ });
+ }
}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index d6814f42..c7621f85 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -7,8 +7,10 @@ import Foundation
import package_info_plus_macos
import path_provider_macos
+import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/macos/Podfile.lock b/macos/Podfile.lock
index e53880d9..e792cbf8 100644
--- a/macos/Podfile.lock
+++ b/macos/Podfile.lock
@@ -4,11 +4,14 @@ PODS:
- FlutterMacOS
- path_provider_macos (0.0.1):
- FlutterMacOS
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
@@ -17,12 +20,15 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
+ url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
-COCOAPODS: 1.9.3
+COCOAPODS: 1.11.2
diff --git a/pubspec.lock b/pubspec.lock
index 41cdef38..edb8e3de 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -35,7 +35,7 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.2.0"
charcode:
dependency: transitive
description:
@@ -125,6 +125,13 @@ 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
@@ -189,6 +196,13 @@ 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:
@@ -411,6 +425,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.0.12"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.2"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.2"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.4"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.4"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.2"
vector_math:
dependency: transitive
description:
@@ -447,5 +503,5 @@ packages:
source: hosted
version: "3.1.0"
sdks:
- dart: ">=2.13.0 <3.0.0"
- flutter: ">=2.0.0"
+ dart: ">=2.14.0 <3.0.0"
+ flutter: ">=2.5.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 366d60a6..68a1e5e6 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -43,6 +43,8 @@ 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:
msix: ^2.1.3