WIP: add experimental clickable links with dialog

(copy / open). Bug remaining for selectable text
This commit is contained in:
Nima Boscarino 2021-11-05 22:38:45 -07:00
parent 4971879b0e
commit ec1dd05ba1
8 changed files with 182 additions and 14 deletions

View File

@ -30,6 +30,11 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
<data android:scheme="http" />
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
const TapirGroupsExperiment = "tapir-groups-experiment"; const TapirGroupsExperiment = "tapir-groups-experiment";
const FileSharingExperiment = "filesharing"; const FileSharingExperiment = "filesharing";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode { enum DualpaneMode {
Single, Single,

View File

@ -204,6 +204,22 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
inactiveTrackColor: settings.theme.defaultButtonDisabledColor(), inactiveTrackColor: settings.theme.defaultButtonDisabledColor(),
secondary: Icon(Icons.attach_file, color: settings.current().mainTextColor()), 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( AboutListTile(

View File

@ -3,9 +3,13 @@ import 'dart:io';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:flutter/material.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 'package:provider/provider.dart';
import '../model.dart'; import '../model.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import '../settings.dart'; import '../settings.dart';
import 'messagebubbledecorations.dart'; import 'messagebubbledecorations.dart';
@ -28,6 +32,7 @@ class MessageBubbleState extends State<MessageBubble> {
var prettyDate = ""; var prettyDate = "";
var borderRadiousEh = 15.0; var borderRadiousEh = 15.0;
// var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString(); // var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp; DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal()); prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
@ -45,16 +50,37 @@ class MessageBubbleState extends State<MessageBubble> {
var wdgSender = SelectableText(senderDisplayStr, var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor())); style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText( var wdgMessage;
widget.content + '\u202F',
//key: Key(myKey), if (!showClickableLinks) {
focusNode: _focus, wdgMessage = SelectableText(
style: TextStyle( widget.content + '\u202F',
color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(), //key: Key(myKey),
), focusNode: _focus,
textAlign: TextAlign.left, style: TextStyle(
textWidthBasis: TextWidthBasis.longestLine, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(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<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor(),
),
textAlign: TextAlign.left,
textWidthBasis: TextWidthBasis.longestLine,
);
}
var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate); var wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, prettyDate: prettyDate);
@ -90,4 +116,58 @@ class MessageBubbleState extends State<MessageBubble> {
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations]))))); children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
}); });
} }
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
showModalBottomSheet<void>(
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: <Widget>[
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: <Widget>[
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';
}
},
),
),
]),
],
)),
));
});
}
} }

View File

@ -7,8 +7,10 @@ import Foundation
import package_info_plus_macos import package_info_plus_macos
import path_provider_macos import path_provider_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -4,11 +4,14 @@ PODS:
- FlutterMacOS - FlutterMacOS
- path_provider_macos (0.0.1): - path_provider_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES: DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - 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`) - 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: EXTERNAL SOURCES:
FlutterMacOS: FlutterMacOS:
@ -17,12 +20,15 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
path_provider_macos: path_provider_macos:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b path_provider_macos: a0a3fd666cb7cd0448e936fb4abad4052961002b
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
COCOAPODS: 1.9.3 COCOAPODS: 1.11.2

View File

@ -35,7 +35,7 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -125,6 +125,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -189,6 +196,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.3" version: "0.6.3"
linkify:
dependency: transitive
description:
name: linkify
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -411,6 +425,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" 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: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -447,5 +503,5 @@ packages:
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.13.0 <3.0.0" dart: ">=2.14.0 <3.0.0"
flutter: ">=2.0.0" flutter: ">=2.5.0"

View File

@ -43,6 +43,8 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
file_picker: ^4.0.1 file_picker: ^4.0.1
file_picker_desktop: ^1.1.0 file_picker_desktop: ^1.1.0
flutter_linkify: ^5.0.2
url_launcher: ^6.0.12
dev_dependencies: dev_dependencies:
msix: ^2.1.3 msix: ^2.1.3