Merge pull request 'Clickable hyperlinks for MessageBubbles' (#235) from NimaBoscarino/cwtch-ui:nima/clickable-links into trunk
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #235 Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca> Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
commit
2c1347e50e
|
@ -48,4 +48,11 @@
|
||||||
|
|
||||||
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
<!--Meeded to check if activity is foregrounded or if messages from the service should be queued-->
|
||||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
const TapirGroupsExperiment = "tapir-groups-experiment";
|
const TapirGroupsExperiment = "tapir-groups-experiment";
|
||||||
const ServerManagementExperiment = "servers-experiment";
|
const ServerManagementExperiment = "servers-experiment";
|
||||||
const FileSharingExperiment = "filesharing";
|
const FileSharingExperiment = "filesharing";
|
||||||
|
const ClickableLinksExperiment = "clickable-links";
|
||||||
|
|
||||||
enum DualpaneMode {
|
enum DualpaneMode {
|
||||||
Single,
|
Single,
|
||||||
|
|
|
@ -227,6 +227,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(
|
||||||
|
|
|
@ -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,7 +50,10 @@ 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;
|
||||||
|
|
||||||
|
if (!showClickableLinks) {
|
||||||
|
wdgMessage = SelectableText(
|
||||||
widget.content + '\u202F',
|
widget.content + '\u202F',
|
||||||
//key: Key(myKey),
|
//key: Key(myKey),
|
||||||
focusNode: _focus,
|
focusNode: _focus,
|
||||||
|
@ -55,6 +63,27 @@ class MessageBubbleState extends State<MessageBubble> {
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
textWidthBasis: TextWidthBasis.longestLine,
|
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()],
|
||||||
|
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);
|
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])))));
|
children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations])))));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _modalOpenLink(BuildContext ctx, LinkableElement link) {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: ctx,
|
||||||
|
builder: (BuildContext bcontext) {
|
||||||
|
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?"
|
||||||
|
),
|
||||||
|
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 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"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
62
pubspec.lock
62
pubspec.lock
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue