Clickable hyperlinks for MessageBubbles #235
NimaBoscarino/cwtch-ui:nima/clickable-links
into trunk
2 years ago
@ -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 2 years ago
Review
Let me know if there's a preferred icon to use! |
|||||||
),
|
|||||||
],
|
|||||||
)),
|
|||||||
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 2 years ago
Review
For some reason, once I add the
sarah
commented 2 years ago
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.
sarah
commented 2 years ago
Review
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).
NimaBoscarino
commented 2 years ago
Review
I tried it with
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 2 years ago
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?
sarah
commented 2 years ago
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.) |
|||||||
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 2 years ago
Review
I'm quite new to styling and layout in Flutter. Does this look okay, or are there cleaner/other ways preferred?
sarah
commented 2 years ago
Review
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';
|
|||||||
}
|
|||||||
},
|
|||||||
),
|
|||||||
),
|
|||||||
]),
|
|||||||
],
|
|||||||
)),
|
|||||||
));
|
|||||||
});
|
|||||||
}
|
|||||||
}
|
|||||||
|
I beleive Sarah you will translate these in a follow up PR?
Yup.