Clickable hyperlinks for MessageBubbles #235

Merged
sarah merged 9 commits from NimaBoscarino/cwtch-ui:nima/clickable-links into trunk 2021-11-17 02:09:06 +00:00
8 changed files with 182 additions and 14 deletions
Showing only changes of commit ec1dd05ba1 - Show all commits

View File

@ -30,6 +30,11 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
<data android:scheme="http" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
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 FileSharingExperiment = "filesharing";
const ClickableLinksExperiment = "clickable-links";
enum DualpaneMode {
Single,

View File

@ -204,6 +204,22 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
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(

View File

@ -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<MessageBubble> {
var prettyDate = "";
var borderRadiousEh = 15.0;
// 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;
prettyDate = DateFormat.yMd(Platform.localeName).add_jm().format(messageDate.toLocal());
@ -45,16 +50,37 @@ class MessageBubbleState extends State<MessageBubble> {
var wdgSender = SelectableText(senderDisplayStr,
style: TextStyle(fontSize: 9.0, color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeTextColor() : Provider.of<Settings>(context).theme.messageFromOtherTextColor()));
var wdgMessage = SelectableText(
widget.content + '\u202F',
//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 wdgMessage;
if (!showClickableLinks) {
wdgMessage = SelectableText(
widget.content + '\u202F',
//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,
);
} else {
wdgMessage = SelectableLinkify(
text: widget.content + '\u202F',
// TODO: onOpen breaks the "selectable" functionality. Maybe something to do with gesture handler?
Review

For some reason, once I add the onOpen handler the text selection breaks (e.g. https://i.imgur.com/SrtOLIG.mp4). I don't know enough about Flutter/Dart to figure it out easily, do you know what the case might be? I tried to recreate it in a fresh sandbox but had no luck, so I'm wondering if maybe the TapGestureRecognizer in Linkify conflicts with the MouseRegion in messagerow.dart?

For some reason, once I add the `onOpen` handler the text selection breaks (e.g. https://i.imgur.com/SrtOLIG.mp4). I don't know enough about Flutter/Dart to figure it out easily, do you know what the case might be? I tried to recreate it in a fresh sandbox but had no luck, so I'm wondering if maybe the `TapGestureRecognizer` in [Linkify](https://github.com/Cretezy/flutter_linkify/blob/1363236817d2c703770fdc7e02ccb4a3754b8ef8/lib/flutter_linkify.dart#L344) conflicts with the `MouseRegion` in `messagerow.dart`?
Review

We've definitely had a few issues with widgets interacting in these kinds of ways. I will take a look into this early next week.

We've definitely had a few issues with widgets interacting in these kinds of ways. I will take a look into this early next week.
Review

I could not replicate the lack of text selection on Linux under the following Flutter Build

Flutter 2.6.0-12.0.pre.637 • channel master • https://github.com/flutter/flutter.git
Framework • revision d6dd2fa78d (15 minutes ago) • 2021-11-08 11:23:34 -0800
Engine • revision 469d6f1a09
Tools • Dart 2.15.0 (build 2.15.0-285.0.dev)

As such I wonder if this is platform specific (or perhaps fixed in a newer version of flutter).

I could not replicate the lack of text selection on Linux under the following Flutter Build Flutter 2.6.0-12.0.pre.637 • channel master • https://github.com/flutter/flutter.git Framework • revision d6dd2fa78d (15 minutes ago) • 2021-11-08 11:23:34 -0800 Engine • revision 469d6f1a09 Tools • Dart 2.15.0 (build 2.15.0-285.0.dev) As such I wonder if this is platform specific (or perhaps fixed in a newer version of flutter).
Review

I tried it with

Flutter 2.6.0-12.0.pre.641 • channel master • https://github.com/flutter/flutter.git
Framework • revision f4f23ecb59 (46 minutes ago) • 2021-11-08 12:27:14 -0800
Engine • revision 469d6f1a09
Tools • Dart 2.15.0 (build 2.15.0-285.0.dev)

and still ran into the issue, so it looks like it might be as macOS-specific issue!

I tried it with ``` Flutter 2.6.0-12.0.pre.641 • channel master • https://github.com/flutter/flutter.git Framework • revision f4f23ecb59 (46 minutes ago) • 2021-11-08 12:27:14 -0800 Engine • revision 469d6f1a09 Tools • Dart 2.15.0 (build 2.15.0-285.0.dev) ``` and still ran into the issue, so it looks like it might be as macOS-specific issue!
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);
@ -90,4 +116,58 @@ class MessageBubbleState extends State<MessageBubble> {
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
Review

I stole this modal from elsewhere in the code. Since the height has been hard-coded to 200 in several places, should I extract the modal out to be its own widget?

I stole this modal from elsewhere in the code. Since the height has been hard-coded to 200 in several places, should I extract the modal out to be its own widget?
Review

I think having this widget defined here right now is fine. We have a separate task planned to go through and check widgets like (with hard coded heights etc.)

I think having this widget defined here right now is fine. We have a separate task planned to go through and check widgets like (with hard coded heights etc.)
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>[
Review

I'm quite new to styling and layout in Flutter. Does this look okay, or are there cleaner/other ways preferred?

I'm quite new to styling and layout in Flutter. Does this look okay, or are there cleaner/other ways preferred?
Review

This looks fine :)

This looks fine :)
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 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"))
}

View File

@ -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

View File

@ -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"

View File

@ -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