Clickable hyperlinks for MessageBubbles #235
|
@ -48,4 +48,11 @@
|
|||
|
||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||
const TapirGroupsExperiment = "tapir-groups-experiment";
|
||||
const ServerManagementExperiment = "servers-experiment";
|
||||
const FileSharingExperiment = "filesharing";
|
||||
const ClickableLinksExperiment = "clickable-links";
|
||||
|
||||
enum DualpaneMode {
|
||||
Single,
|
||||
|
|
|
@ -227,6 +227,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()),
|
||||
NimaBoscarino
commented
Let me know if there's a preferred icon to use! Let me know if there's a preferred icon to use!
sarah
commented
Our staff designer @marcia will create an icon for this :) Our staff designer @marcia will create an icon for this :)
|
||||
),
|
||||
],
|
||||
)),
|
||||
AboutListTile(
|
||||
|
|
|
@ -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,40 @@ 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?
|
||||
NimaBoscarino
commented
For some reason, once I add the 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`?
sarah
commented
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.
sarah
commented
I could not replicate the lack of text selection on Linux under the following Flutter Build
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).
NimaBoscarino
commented
I tried it with
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()],
|
||||
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(),
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Provider.of<Settings>(context).current().mainTextColor(),
|
||||
),
|
||||
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 +119,57 @@ 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) {
|
||||
NimaBoscarino
commented
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?
sarah
commented
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.)
|
||||
return Container(
|
||||
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?"
|
||||
),
|
||||
NimaBoscarino
commented
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?
sarah
commented
This looks fine :) This looks fine :)
|
||||
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));
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.copiedClipboardNotification),
|
||||
);
|
||||
|
||||
Navigator.pop(bcontext);
|
||||
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';
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
],
|
||||
)),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
62
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"
|
||||
|
|
|
@ -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
|
||||
|
|
I beleive Sarah you will translate these in a follow up PR?
Yup.