From cb582751b254f3d6d27fccb2f6d9c63aadc982df Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 28 Dec 2022 19:44:52 -0800 Subject: [PATCH 1/3] Ensure all Scrollbars have dedicated Controllers Fix: #602 --- lib/views/addcontactview.dart | 7 ++++++- lib/views/addeditprofileview.dart | 5 ++++- lib/views/addeditservers.dart | 5 ++++- lib/views/globalsettingsview.dart | 6 +++++- lib/views/groupsettingsview.dart | 5 ++++- lib/views/peersettingsview.dart | 5 ++++- lib/views/profileserversview.dart | 5 ++++- lib/views/torstatusview.dart | 4 +++- lib/widgets/messagerow.dart | 4 +++- lib/widgets/remoteserverrow.dart | 6 +++--- 10 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index 921ee84d..1b83c0b4 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -114,9 +114,12 @@ class _AddContactViewState extends State { /// The Add Peer Tab allows a peer to add a specific non-group peer to their contact lists /// We also provide a convenient way to copy their onion. Widget addPeerTab(bcontext) { + ScrollController controller = ScrollController(); return Scrollbar( + controller: controller, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: controller, child: Container( margin: EdgeInsets.all(30), padding: EdgeInsets.all(20), @@ -187,10 +190,12 @@ class _AddContactViewState extends State { if (Provider.of(bcontext).serverList.servers.isEmpty) { return Text(AppLocalizations.of(bcontext)!.addServerFirst); } - + ScrollController controller = ScrollController(); return Scrollbar( + controller: controller, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: controller, child: Container( margin: EdgeInsets.all(30), padding: EdgeInsets.all(20), diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 7a48cac6..f1041d11 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -38,6 +38,7 @@ class _AddEditProfileViewState extends State { final ctrlrPass = TextEditingController(text: ""); final ctrlrPass2 = TextEditingController(text: ""); final ctrlrOnion = TextEditingController(text: ""); + ScrollController controller = ScrollController(); late bool usePassword; late bool deleted; @@ -75,8 +76,10 @@ class _AddEditProfileViewState extends State { return Consumer(builder: (context, theme, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: controller, child: SingleChildScrollView( + controller: controller, clipBehavior: Clip.antiAlias, child: ConstrainedBox( constraints: BoxConstraints( diff --git a/lib/views/addeditservers.dart b/lib/views/addeditservers.dart index 23461e06..978c8573 100644 --- a/lib/views/addeditservers.dart +++ b/lib/views/addeditservers.dart @@ -67,11 +67,14 @@ class _AddEditServerViewState extends State { } Widget _buildSettingsList() { + ScrollController controller = ScrollController(); return Consumer2(builder: (context, serverInfoState, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: controller, child: SingleChildScrollView( + controller: controller, clipBehavior: Clip.antiAlias, child: ConstrainedBox( constraints: BoxConstraints( diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 29c45fd8..924e18cd 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -36,6 +36,8 @@ class _GlobalSettingsViewState extends State { static const androidSettingsChangeChannel = const MethodChannel('androidSettingsChanged'); bool powerExempt = false; + ScrollController settingsListScrollController = ScrollController(); + @override void dispose() { super.dispose(); @@ -95,9 +97,11 @@ class _GlobalSettingsViewState extends State { var appIcon = Icon(Icons.info, color: settings.current().mainTextColor); return Scrollbar( key: Key("SettingsView"), - isAlwaysShown: true, + trackVisibility: true, + controller: settingsListScrollController, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: settingsListScrollController, padding: EdgeInsets.all(20), child: ConstrainedBox( constraints: BoxConstraints( diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 032ed800..65048f7d 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -27,6 +27,7 @@ class _GroupSettingsViewState extends State { final ctrlrNick = TextEditingController(text: ""); final ctrlrGroupAddr = TextEditingController(text: ""); + ScrollController groupSettingsScrollController = ScrollController(); @override void initState() { @@ -55,9 +56,11 @@ class _GroupSettingsViewState extends State { return Consumer(builder: (context, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: groupSettingsScrollController, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: groupSettingsScrollController, child: ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 7d4c029e..d4ae50c0 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -28,6 +28,7 @@ class _PeerSettingsViewState extends State { } final ctrlrNick = TextEditingController(text: ""); + ScrollController peerSettingsScrollController = ScrollController(); @override void initState() { @@ -83,9 +84,11 @@ class _PeerSettingsViewState extends State { } return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: peerSettingsScrollController, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: peerSettingsScrollController, child: ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index e897696e..8e115c31 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -76,10 +76,13 @@ class _ProfileServersView extends State { ))); return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { + ScrollController controller = ScrollController(); return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: controller, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, + controller: controller, child: Container( margin: EdgeInsets.fromLTRB(5, 0, 5, 10), padding: EdgeInsets.fromLTRB(5, 0, 5, 10), diff --git a/lib/views/torstatusview.dart b/lib/views/torstatusview.dart index 5abdee60..beb2c601 100644 --- a/lib/views/torstatusview.dart +++ b/lib/views/torstatusview.dart @@ -21,6 +21,7 @@ class _TorStatusView extends State { TextEditingController torSocksPortController = TextEditingController(); TextEditingController torControlPortController = TextEditingController(); TextEditingController torConfigController = TextEditingController(); + ScrollController torScrollContoller = ScrollController(); @override void dispose() { @@ -52,7 +53,8 @@ class _TorStatusView extends State { return Consumer(builder: (context, torStatus, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: torScrollContoller, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, child: ConstrainedBox( diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 9da8885b..132664f6 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -371,6 +371,7 @@ void modalShowReplies( context: ctx, builder: (BuildContext bcontext) { List replies = getReplies(cache, messageID); + ScrollController controller = ScrollController(); return ChangeNotifierProvider.value( value: profile, @@ -432,7 +433,8 @@ void modalShowReplies( } return Scrollbar( - isAlwaysShown: true, + trackVisibility: true, + controller: controller, child: SingleChildScrollView( clipBehavior: Clip.antiAlias, child: ConstrainedBox( diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart index a3d20207..26258004 100644 --- a/lib/widgets/remoteserverrow.dart +++ b/lib/widgets/remoteserverrow.dart @@ -40,9 +40,9 @@ class _RemoteServerRowState extends State { Visibility( visible: !running, child: Icon( - CwtchIcons.negative_heart_24px, - color: Provider.of(context).theme.portraitOfflineBorderColor, - )), + CwtchIcons.negative_heart_24px, + color: Provider.of(context).theme.portraitOfflineBorderColor, + )), ])), Expanded( child: Column( -- 2.25.1 From e37fb54fd46731d4a52130b0c8f8e960eca9e136 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 29 Dec 2022 12:48:12 -0800 Subject: [PATCH 2/3] Allow Links to be Selectable / Fix Performance of Message Row to prevent Spurious Renders --- lib/main.dart | 1 + lib/third_party/linkify/flutter_linkify.dart | 47 +++++++++++--------- lib/widgets/messagerow.dart | 14 +++--- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 0eb5c049..71577a93 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -123,6 +123,7 @@ class FlwtchState extends State with WindowListener { key: Key('app'), navigatorKey: navKey, locale: settings.locale, + showPerformanceOverlay: false, localizationsDelegates: >[ AppLocalizations.delegate, MaterialLocalizationDelegate(), diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 527c2685..589d5309 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -122,12 +122,15 @@ class Linkify extends StatelessWidget { linkifiers: linkifiers, ); - return Text.rich( - buildTextSpan( + return SelectionArea( + child: RichText( + selectionRegistrar: SelectionContainer.maybeOf(context), + text: buildTextSpan( elements, style: Theme.of(context).textTheme.bodyText2?.merge(style), onOpen: onOpen, useMouseRegion: true, + context: context, linkStyle: Theme.of(context) .textTheme .bodyText2 @@ -148,7 +151,7 @@ class Linkify extends StatelessWidget { locale: locale, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, - ); + )); } } @@ -297,6 +300,7 @@ class SelectableLinkify extends StatelessWidget { style: Theme.of(context).textTheme.bodyText2?.merge(style), codeStyle: Theme.of(context).textTheme.bodyText2?.merge(codeStyle), onOpen: onOpen, + context: context, linkStyle: Theme.of(context) .textTheme .bodyText2 @@ -337,11 +341,13 @@ class LinkableSpan extends WidgetSpan { LinkableSpan({ required MouseCursor mouseCursor, required InlineSpan inlineSpan, + required BuildContext context, }) : super( child: MouseRegion( cursor: mouseCursor, - child: Text.rich( - inlineSpan, + child: RichText( + text: inlineSpan, + selectionRegistrar: SelectionContainer.maybeOf(context), ), ), ); @@ -354,6 +360,7 @@ TextSpan buildTextSpan( TextStyle? linkStyle, TextStyle? codeStyle, LinkCallback? onOpen, + required BuildContext context, bool useMouseRegion = false, }) { return TextSpan( @@ -361,20 +368,18 @@ TextSpan buildTextSpan( (element) { if (element is LinkableElement) { if (useMouseRegion) { - return TooltipSpan( - message: element.url, - inlineSpan: LinkableSpan( - mouseCursor: SystemMouseCursors.click, - inlineSpan: TextSpan(text: element.text, style: linkStyle, recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, semanticsLabel: element.text), - )); + return TextSpan( + text: element.text, + style: linkStyle, + mouseCursor: SystemMouseCursors.click, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + semanticsLabel: element.text); } else { - return TooltipSpan( - message: element.url, - inlineSpan: TextSpan( - text: element.text, - style: linkStyle, - recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, - )); + return TextSpan( + text: element.text, + style: linkStyle, + recognizer: onOpen != null ? (TapGestureRecognizer()..onTap = () => onOpen(element)) : null, + ); } } else if (element is BoldElement) { return TextSpan(text: element.text.replaceAll("*", ""), style: style?.copyWith(fontWeight: FontWeight.bold), semanticsLabel: element.text); @@ -428,11 +433,13 @@ class TooltipSpan extends WidgetSpan { TooltipSpan({ required String message, required InlineSpan inlineSpan, + required BuildContext context, }) : super( child: Tooltip( message: message, - child: Text.rich( - inlineSpan, + child: RichText( + text: inlineSpan, + selectionRegistrar: SelectionContainer.maybeOf(context), ), ), ); diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 132664f6..a0fa87af 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -215,14 +215,16 @@ class MessageRowState extends State with SingleTickerProviderStateMi var mr = MouseRegion( // For desktop... onHover: (event) { - setState(() { - Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageID; - }); + if (Provider.of(context, listen: false).hoveredIndex != Provider.of(context, listen: false).messageID) { + setState(() { + Provider.of(context, listen: false).hoveredIndex = Provider.of(context, listen: false).messageID; + }); + } }, onExit: (event) { - setState(() { - Provider.of(context, listen: false).hoveredIndex = -1; - }); + // setState(() { + // Provider.of(context, listen: false).hoveredIndex = -1; + //}); }, child: GestureDetector( onPanUpdate: (details) { -- 2.25.1 From 1c79e0842c702df1368bebd3f36e73e042d70a2d Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 29 Dec 2022 20:27:25 -0800 Subject: [PATCH 3/3] Fix debug layout errors + Fix Quoted File Message Layout --- lib/models/messages/filemessage.dart | 5 ++--- lib/models/messages/textmessage.dart | 6 +++--- lib/third_party/linkify/flutter_linkify.dart | 1 + lib/views/messageview.dart | 2 ++ lib/widgets/filebubble.dart | 12 ++++++++++-- lib/widgets/quotedmessage.dart | 13 ++++--------- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index e3e972c9..c60f222e 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -59,7 +59,7 @@ class FileMessage extends Message { if (shareObj == null) { return MessageRow(MalformedBubble(), 0); } - String nameSuggestion = shareObj['n'] as String; + String nameSuggestion = shareObj['f'] as String; String rootHash = shareObj['h'] as String; String nonce = shareObj['n'] as String; int fileSize = shareObj['s'] as int; @@ -68,8 +68,7 @@ class FileMessage extends Message { } return Container( alignment: Alignment.center, - width: 50, - height: 50, + height: 100, child: FileBubble( nameSuggestion, rootHash, diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index 3c46a21a..17e8cc3a 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -28,12 +28,12 @@ class TextMessage extends Message { return SelectableLinkify( text: content + '\u202F', options: LinkifyOptions(messageFormatting: formatMessages, parseLinks: false, looseUrl: true, defaultToHttps: true), - linkifiers: [UrlLinkifier()], + linkifiers: [], onOpen: null, textAlign: TextAlign.left, - style: TextStyle(overflow: TextOverflow.ellipsis), + style: TextStyle(overflow: TextOverflow.fade), codeStyle: TextStyle(overflow: TextOverflow.ellipsis), - textWidthBasis: TextWidthBasis.longestLine, + textWidthBasis: TextWidthBasis.parent, ); }); } diff --git a/lib/third_party/linkify/flutter_linkify.dart b/lib/third_party/linkify/flutter_linkify.dart index 589d5309..a4c38881 100644 --- a/lib/third_party/linkify/flutter_linkify.dart +++ b/lib/third_party/linkify/flutter_linkify.dart @@ -333,6 +333,7 @@ class SelectableLinkify extends StatelessWidget { cursorHeight: cursorHeight, selectionControls: selectionControls, onSelectionChanged: onSelectionChanged, + style: style, ); } } diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 3010fa40..f74f9f93 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -149,6 +149,8 @@ class _MessageViewState extends State { backgroundColor: Provider.of(context).theme.backgroundMainColor, floatingActionButton: showDown ? FloatingActionButton( + // heroTags need to be unique per screen (important when we pop up and down)... + heroTag: "popDown" + Provider.of(context, listen: false).onion, child: Icon(Icons.arrow_downward, color: Provider.of(context).current().defaultButtonTextColor), onPressed: () { Provider.of(context, listen: false).initialScrollIndex = 0; diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index 78781c8f..0b98423f 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'package:cwtch/config.dart'; +import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/filedownloadprogress.dart'; import 'package:cwtch/models/message.dart'; @@ -131,6 +132,13 @@ class FileBubbleState extends State { // we don't preview a non downloaded file... if (widget.isPreview && myFile != null) { return getPreview(context); + } else if (widget.isPreview && myFile == null) { + return Row( + children: [ + Icon(CwtchIcons.attached_file_2, size: 32, color: Provider.of(context).theme.messageFromMeTextColor), + Flexible(child: Text(widget.nameSuggestion, style: TextStyle(color: Provider.of(context).theme.messageFromMeTextColor))) + ], + ); } return LayoutBuilder(builder: (bcontext, constraints) { @@ -329,7 +337,7 @@ class FileBubbleState extends State { maxLines: 4, textWidthBasis: TextWidthBasis.parent, ), - leading: Icon(Icons.attach_file, size: 32, color: Provider.of(context).theme.messageFromMeTextColor)); + leading: Icon(CwtchIcons.attached_file_2, size: 32, color: Provider.of(context).theme.messageFromMeTextColor)); } // Construct an file chrome @@ -377,7 +385,7 @@ class FileBubbleState extends State { maxLines: 4, textWidthBasis: TextWidthBasis.parent, ), - leading: Icon(Icons.attach_file, size: 32, color: Provider.of(context).theme.messageFromOtherTextColor), + leading: Icon(CwtchIcons.attached_file_2, size: 32, color: Provider.of(context).theme.messageFromOtherTextColor), trailing: Visibility( visible: speed != "0 B/s", child: SelectableText( diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index acf218f9..d371d3e8 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -125,16 +125,11 @@ class QuotedMessageBubbleState extends State { height: 75, child: Column(children: [ Align(alignment: Alignment.centerLeft, child: wdgReplyingTo), - Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ + Flexible( + child: Row(mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(Icons.reply, size: 32, color: qTextColor)), - Flexible( - child: DefaultTextStyle( - textWidthBasis: TextWidthBasis.parent, - child: qMessage.getPreviewWidget(context), - style: TextStyle(color: qTextColor), - overflow: TextOverflow.fade, - )) - ]) + Flexible(child: qMessage.getPreviewWidget(context)), + ])) ])))); } catch (e) { return MalformedBubble(); -- 2.25.1