LCOV - code coverage report
Current view: top level - lib/views - messageview.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 591 0.0 %
Date: 2026-07-15 19:28:30 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : import 'dart:io';
       4             : import 'dart:math';
       5             : import 'package:crypto/crypto.dart';
       6             : import 'package:cwtch/cwtch/cwtch.dart';
       7             : import 'package:cwtch/cwtch_icons_icons.dart';
       8             : import 'package:cwtch/models/appstate.dart';
       9             : import 'package:cwtch/models/chatmessage.dart';
      10             : import 'package:cwtch/models/contact.dart';
      11             : import 'package:cwtch/models/message.dart';
      12             : import 'package:cwtch/models/messagecache.dart';
      13             : import 'package:cwtch/models/messages/quotedmessage.dart';
      14             : import 'package:cwtch/models/profile.dart';
      15             : import 'package:cwtch/models/search.dart';
      16             : import 'package:cwtch/themes/opaque.dart';
      17             : import 'package:cwtch/third_party/linkify/flutter_linkify.dart';
      18             : import 'package:cwtch/widgets/conversation_options.dart';
      19             : import 'package:cwtch/widgets/malformedbubble.dart';
      20             : import 'package:cwtch/widgets/messageloadingbubble.dart';
      21             : import 'package:cwtch/widgets/profileimage.dart';
      22             : import 'package:cwtch/controllers/filesharing.dart' as filesharing;
      23             : import 'package:cwtch/widgets/staticmessagebubble.dart';
      24             : import 'package:flutter/material.dart';
      25             : import 'package:cwtch/views/peersettingsview.dart';
      26             : import 'package:cwtch/widgets/DropdownContacts.dart';
      27             : import 'package:flutter/services.dart';
      28             : import 'package:provider/provider.dart';
      29             : import 'package:cwtch/l10n/app_localizations.dart';
      30             : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
      31             : 
      32             : import '../config.dart';
      33             : import '../constants.dart';
      34             : import '../main.dart';
      35             : import '../settings.dart';
      36             : import '../widgets/messagelist.dart';
      37             : import 'filesharingview.dart';
      38             : import 'groupsettingsview.dart';
      39             : 
      40             : class MessageView extends StatefulWidget {
      41           0 :   @override
      42           0 :   _MessageViewState createState() => _MessageViewState();
      43             : }
      44             : 
      45             : class _MessageViewState extends State<MessageView> {
      46             :   final focusNode = FocusNode();
      47             :   int selectedContact = -1;
      48             :   ItemPositionsListener scrollListener = ItemPositionsListener.create();
      49             :   File? imagePreview;
      50             :   bool showDown = false;
      51             :   bool showPreview = false;
      52             :   final scaffoldKey = GlobalKey<ScaffoldState>(); // <---- Another instance variable
      53           0 :   @override
      54             :   void initState() {
      55           0 :     scrollListener.itemPositions.addListener(() {
      56           0 :       if (scrollListener.itemPositions.value.length != 0 &&
      57           0 :           Provider.of<AppState>(context, listen: false).unreadMessagesBelow == true &&
      58           0 :           scrollListener.itemPositions.value.any((element) => element.index == 0)) {
      59           0 :         Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
      60           0 :         Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
      61             :       }
      62             : 
      63           0 :       if (scrollListener.itemPositions.value.length != 0 && !scrollListener.itemPositions.value.any((element) => element.index == 0)) {
      64           0 :         showDown = true;
      65             :       } else {
      66           0 :         showDown = false;
      67             :       }
      68             :     });
      69           0 :     super.initState();
      70             :   }
      71             : 
      72           0 :   @override
      73             :   void didChangeDependencies() {
      74           0 :     var appState = Provider.of<AppState>(context, listen: false);
      75             : 
      76             :     // using "8" because "# of messages that fit on one screen" isnt trivial to calculate at this point
      77           0 :     if (appState.initialScrollIndex > 4 && appState.unreadMessagesBelow == false) {
      78           0 :       WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((timeStamp) {
      79           0 :         appState.unreadMessagesBelow = true;
      80             :       });
      81             :     }
      82           0 :     super.didChangeDependencies();
      83             :   }
      84             : 
      85           0 :   @override
      86             :   void dispose() {
      87           0 :     focusNode.dispose();
      88           0 :     super.dispose();
      89             :   }
      90             : 
      91           0 :   @override
      92             :   Widget build(BuildContext context) {
      93             :     // After leaving a conversation the selected conversation is set to null...
      94           0 :     if (Provider.of<ContactInfoState>(context, listen: false).profileOnion == "") {
      95           0 :       return Container(
      96           0 :         color: Provider.of<Settings>(context).theme.backgroundMainColor,
      97           0 :         child: Center(child: Text(AppLocalizations.of(context)!.addContactFirst)),
      98             :       );
      99             :     }
     100             : 
     101           0 :     var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
     102           0 :     var showFileSharing = Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment);
     103           0 :     var appBarButtons = <Widget>[];
     104             : 
     105           0 :     var profile = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
     106           0 :     var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
     107             : 
     108           0 :     if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsBlodeuweddSupported() && Provider.of<Settings>(context).isExperimentEnabled(BlodeuweddExperiment)) {
     109           0 :       appBarButtons.add(
     110           0 :         IconButton(
     111           0 :           splashRadius: Material.defaultSplashRadius / 2,
     112           0 :           icon: Icon(Icons.summarize),
     113           0 :           tooltip: AppLocalizations.of(context)!.blodeuweddSummarize,
     114           0 :           onPressed: () async {
     115           0 :             Provider.of<ContactInfoState>(context, listen: false).summary = "";
     116           0 :             Provider.of<ContactInfoState>(context, listen: false).updateSummaryEvent("");
     117           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch.SummarizeConversation(profile, conversation);
     118           0 :             _summarizeConversation(context, Provider.of<ProfileInfoState>(context, listen: false), Provider.of<Settings>(context, listen: false));
     119             :           },
     120             :         ),
     121             :       );
     122             :     }
     123             : 
     124           0 :     appBarButtons.add(ConversationOptions(_pushContactSettings, _pushFileSharingSettings));
     125             : 
     126           0 :     var appState = Provider.of<AppState>(context);
     127           0 :     return PopScope(
     128           0 :       onPopInvoked: _onWillPop,
     129           0 :       child: Scaffold(
     130           0 :         backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor,
     131           0 :         floatingActionButton: showDown
     132           0 :             ? FloatingActionButton(
     133             :                 // heroTags need to be unique per screen (important when we pop up and down)...
     134           0 :                 heroTag: "popDown" + Provider.of<ContactInfoState>(context, listen: false).onion,
     135           0 :                 child: Icon(Icons.arrow_downward, color: Provider.of<Settings>(context).current().defaultButtonTextColor),
     136           0 :                 onPressed: () {
     137           0 :                   Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
     138           0 :                   Provider.of<AppState>(context, listen: false).unreadMessagesBelow = false;
     139           0 :                   Provider.of<ContactInfoState>(context, listen: false).messageScrollController.scrollTo(index: 0, duration: Duration(milliseconds: 600));
     140             :                 },
     141             :               )
     142             :             : null,
     143           0 :         appBar: AppBar(
     144             :           // setting leading(Width) to null makes it do the default behaviour; container() hides it
     145           0 :           leadingWidth: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? 0 : null,
     146           0 :           leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container(padding: EdgeInsets.zero, margin: EdgeInsets.zero, width: 0, height: 0) : null,
     147           0 :           title: Row(
     148           0 :             children: [
     149           0 :               ProfileImage(
     150           0 :                 imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
     151           0 :                     ? Provider.of<ContactInfoState>(context).imagePath
     152           0 :                     : Provider.of<ContactInfoState>(context).defaultImagePath,
     153             :                 diameter: 42,
     154           0 :                 border: Provider.of<ContactInfoState>(context).getBorderColor(Provider.of<Settings>(context).theme),
     155           0 :                 disabled: !Provider.of<ContactInfoState>(context).isOnline(),
     156             :                 badgeTextColor: Colors.red,
     157           0 :                 badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor,
     158           0 :                 badgeIcon: Provider.of<ContactInfoState>(context).isGroup
     159           0 :                     ? (Tooltip(
     160           0 :                         message: Provider.of<ContactInfoState>(context).isOnline()
     161           0 :                             ? Provider.of<ContactInfoState>(context).antispamTickets == 0
     162           0 :                                   ? AppLocalizations.of(context)!.acquiringTicketsFromServer
     163           0 :                                   : AppLocalizations.of(context)!.acquiredTicketsFromServer
     164           0 :                             : AppLocalizations.of(context)!.serverConnectivityDisconnected,
     165           0 :                         child: Provider.of<ContactInfoState>(context).isOnline()
     166           0 :                             ? Provider.of<ContactInfoState>(context).antispamTickets == 0
     167           0 :                                   ? Icon(
     168             :                                       CwtchIcons.anti_spam_3,
     169             :                                       size: 14.0,
     170           0 :                                       semanticLabel: AppLocalizations.of(context)!.acquiringTicketsFromServer,
     171           0 :                                       color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
     172             :                                     )
     173           0 :                                   : Icon(CwtchIcons.anti_spam_2, color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor, size: 14.0)
     174           0 :                             : Icon(CwtchIcons.onion_off, color: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor, size: 14.0),
     175             :                       ))
     176             :                     : null,
     177             :               ),
     178           0 :               SizedBox(width: 10),
     179           0 :               Expanded(
     180           0 :                 child: Container(
     181             :                   height: 42,
     182             :                   clipBehavior: Clip.hardEdge,
     183           0 :                   decoration: BoxDecoration(),
     184           0 :                   child: Align(
     185             :                     alignment: Alignment.centerLeft,
     186           0 :                     child: Text(
     187           0 :                       Provider.of<ContactInfoState>(context).augmentedNickname(context),
     188           0 :                       style: TextStyle(fontFamily: "Inter", fontWeight: FontWeight.bold, fontSize: 14.0 * Provider.of<Settings>(context).fontScaling),
     189             :                       overflow: TextOverflow.clip,
     190             :                       maxLines: 1,
     191             :                     ),
     192             :                   ),
     193             :                 ),
     194             :               ),
     195             :             ],
     196             :           ),
     197             :           actions: appBarButtons,
     198             :         ),
     199           0 :         body: Padding(padding: EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 164.0), child: MessageList(scrollListener)),
     200           0 :         bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(context),
     201             :       ),
     202             :     );
     203             :   }
     204             : 
     205           0 :   Future<bool> _onWillPop(popd) async {
     206           0 :     Provider.of<ContactInfoState>(context, listen: false).unreadMessages = 0;
     207             : 
     208           0 :     var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
     209             :     if (previouslySelected != null) {
     210           0 :       Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(previouslySelected)!.unselected();
     211             :     }
     212           0 :     Provider.of<SearchState>(context, listen: false).clearSearch();
     213           0 :     Provider.of<AppState>(context, listen: false).selectedConversation = null;
     214           0 :     Provider.of<AppState>(context, listen: false).initialScrollIndex = 0;
     215             :     return true;
     216             :   }
     217             : 
     218           0 :   void _pushFileSharingSettings() {
     219           0 :     var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
     220           0 :     var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
     221           0 :     Navigator.of(context).push(
     222           0 :       PageRouteBuilder(
     223           0 :         pageBuilder: (builderContext, a1, a2) {
     224           0 :           return MultiProvider(
     225           0 :             providers: [
     226           0 :               ChangeNotifierProvider.value(value: profileInfoState),
     227           0 :               ChangeNotifierProvider.value(value: contactInfoState),
     228             :             ],
     229           0 :             child: FileSharingView(),
     230             :           );
     231             :         },
     232           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     233           0 :         transitionDuration: Duration(milliseconds: 200),
     234             :       ),
     235             :     );
     236             :   }
     237             : 
     238           0 :   void _pushContactSettings() {
     239           0 :     var profileInfoState = Provider.of<ProfileInfoState>(context, listen: false);
     240           0 :     var contactInfoState = Provider.of<ContactInfoState>(context, listen: false);
     241             : 
     242           0 :     if (Provider.of<ContactInfoState>(context, listen: false).isGroup == true) {
     243           0 :       Navigator.of(context).push(
     244           0 :         PageRouteBuilder(
     245           0 :           pageBuilder: (builderContext, a1, a2) {
     246           0 :             return MultiProvider(
     247           0 :               providers: [
     248           0 :                 ChangeNotifierProvider.value(value: profileInfoState),
     249           0 :                 ChangeNotifierProvider.value(value: contactInfoState),
     250             :               ],
     251           0 :               child: GroupSettingsView(),
     252             :             );
     253             :           },
     254           0 :           transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     255           0 :           transitionDuration: Duration(milliseconds: 200),
     256             :         ),
     257             :       );
     258             :     } else {
     259           0 :       Navigator.of(context).push(
     260           0 :         PageRouteBuilder(
     261           0 :           pageBuilder: (builderContext, a1, a2) {
     262           0 :             return MultiProvider(
     263           0 :               providers: [
     264           0 :                 ChangeNotifierProvider.value(value: profileInfoState),
     265           0 :                 ChangeNotifierProvider.value(value: contactInfoState),
     266             :               ],
     267           0 :               child: PeerSettingsView(),
     268             :             );
     269             :           },
     270           0 :           transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     271           0 :           transitionDuration: Duration(milliseconds: 200),
     272             :         ),
     273             :       );
     274             :     }
     275             :   }
     276             : 
     277             :   // todo: legacy groups currently have restricted message
     278             :   // size because of the additional wrapping end encoding
     279             :   // hybrid groups should allow these numbers to be the same.
     280             :   static const P2PMessageLengthMax = 7000;
     281             :   static const GroupMessageLengthMax = 1600;
     282             : 
     283           0 :   void _sendMessage([String? ignoredParam]) {
     284             :     // Do this after we trim to preserve enter-behaviour...
     285           0 :     bool cannotSend = Provider.of<ContactInfoState>(context, listen: false).canSend() == false;
     286           0 :     bool isGroup = Provider.of<ContactInfoState>(context, listen: false).isGroup;
     287             :     if (cannotSend) {
     288             :       return;
     289             :     }
     290             : 
     291           0 :     var attachedInvite = Provider.of<ContactInfoState>(context, listen: false).messageDraft.getInviteHandle();
     292             :     if (attachedInvite != null) {
     293           0 :       this._sendInvitation(attachedInvite);
     294             :     }
     295             : 
     296             :     // Trim message
     297           0 :     var messageText = Provider.of<ContactInfoState>(context, listen: false).messageDraft.messageText ?? "";
     298           0 :     final messageWithoutNewLine = messageText.trimRight();
     299             : 
     300             :     // peers and groups currently have different length constraints (servers can store less)...
     301           0 :     var actualMessageLength = messageText.length;
     302           0 :     var lengthOk = (isGroup && actualMessageLength < GroupMessageLengthMax) || actualMessageLength <= P2PMessageLengthMax;
     303             : 
     304           0 :     if (messageWithoutNewLine.isNotEmpty && lengthOk) {
     305           0 :       if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage() != null) {
     306           0 :         var conversationId = Provider.of<AppState>(context, listen: false).selectedConversation!;
     307           0 :         MessageCache? cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationId)?.messageCache;
     308           0 :         ById(
     309           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage()!.index,
     310           0 :         ).get(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(context, listen: false).selectedProfile!, conversationId, cache!).then((MessageInfo? data) {
     311             :           try {
     312           0 :             var bytes1 = utf8.encode(data!.metadata.senderHandle + data.wrapper);
     313           0 :             var digest1 = sha256.convert(bytes1);
     314           0 :             var contentHash = base64Encode(digest1.bytes);
     315           0 :             var quotedMessage = jsonEncode(QuotedMessageStructure(contentHash, messageWithoutNewLine));
     316           0 :             ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
     317           0 :             Provider.of<FlwtchState>(context, listen: false).cwtch
     318           0 :                 .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
     319           0 :                 .then(_sendMessageHandler);
     320             :           } catch (e) {
     321           0 :             EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
     322             :           }
     323           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
     324             :         });
     325             :       } else {
     326           0 :         ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: messageWithoutNewLine);
     327           0 :         Provider.of<FlwtchState>(context, listen: false).cwtch
     328           0 :             .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm))
     329           0 :             .then(_sendMessageHandler);
     330             :       }
     331             :     }
     332             :   }
     333             : 
     334           0 :   void _sendInvitation(int contact) {
     335           0 :     Provider.of<FlwtchState>(
     336           0 :       context,
     337             :       listen: false,
     338           0 :     ).cwtch.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, contact).then(_sendMessageHandler);
     339             :   }
     340             : 
     341           0 :   void _sendFile(String filePath) {
     342           0 :     Provider.of<FlwtchState>(
     343           0 :       context,
     344             :       listen: false,
     345           0 :     ).cwtch.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath).then(_sendMessageHandler);
     346             :   }
     347             : 
     348           0 :   void _sendMessageHandler(dynamic messageJson) {
     349           0 :     if (Provider.of<ContactInfoState>(context, listen: false).isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) {
     350           0 :       final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.acquiringTicketsFromServer));
     351           0 :       ScaffoldMessenger.of(context).showSnackBar(snackBar);
     352             :       return;
     353             :     }
     354             : 
     355             :     // At this point we have decided to send the text to the backend, failure is still possible
     356             :     // but it will show as an error-ed message, as such the draft can be purged.
     357           0 :     Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
     358             : 
     359           0 :     var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
     360           0 :     var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
     361           0 :     var profile = Provider.of<ProfileInfoState>(context, listen: false);
     362             : 
     363           0 :     var messageInfo = messageJsonToInfo(profileOnion, identifier, messageJson);
     364             :     if (messageInfo != null) {
     365           0 :       profile.newMessage(
     366           0 :         messageInfo.metadata.conversationIdentifier,
     367           0 :         messageInfo.metadata.messageID,
     368           0 :         messageInfo.metadata.timestamp,
     369           0 :         messageInfo.metadata.senderHandle,
     370           0 :         messageInfo.metadata.senderImage ?? "",
     371           0 :         messageInfo.metadata.isAuto,
     372           0 :         messageInfo.wrapper,
     373           0 :         messageInfo.metadata.contenthash,
     374             :         true,
     375             :         true,
     376             :       );
     377             :     }
     378             : 
     379           0 :     Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileOnion, identifier, LastMessageSeenTimeKey, DateTime.now().toIso8601String());
     380           0 :     focusNode.requestFocus();
     381           0 :     Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
     382             :   }
     383             : 
     384           0 :   Widget senderInviteChrome(String chrome, String targetName) {
     385           0 :     var settings = Provider.of<Settings>(context);
     386             : 
     387           0 :     return Wrap(
     388           0 :       children: [
     389           0 :         SelectableText(
     390           0 :           chrome + '\u202F',
     391           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     392             :           textAlign: TextAlign.left,
     393             :           maxLines: 2,
     394             :           textWidthBasis: TextWidthBasis.longestLine,
     395             :         ),
     396           0 :         SelectableText(
     397           0 :           targetName + '\u202F',
     398           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     399             :           textAlign: TextAlign.left,
     400             :           maxLines: 2,
     401             :           textWidthBasis: TextWidthBasis.longestLine,
     402             :         ),
     403             :       ],
     404             :     );
     405             :   }
     406             : 
     407           0 :   Widget _buildPreviewBox() {
     408           0 :     var showClickableLinks = Provider.of<Settings>(context).isExperimentEnabled(ClickableLinksExperiment);
     409             : 
     410           0 :     var wdgMessage = Padding(
     411           0 :       padding: EdgeInsets.all(8),
     412           0 :       child: SelectableLinkify(
     413           0 :         text: Provider.of<ContactInfoState>(context).messageDraft.messageText + '\n',
     414           0 :         options: LinkifyOptions(messageFormatting: true, parseLinks: showClickableLinks, looseUrl: true, defaultToHttps: true),
     415           0 :         linkifiers: [UrlLinkifier()],
     416             :         onOpen: showClickableLinks ? null : null,
     417           0 :         style: TextStyle(
     418           0 :           color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
     419             :           fontFamily: "Inter",
     420             :           fontWeight: FontWeight.normal,
     421           0 :           fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
     422             :         ),
     423           0 :         linkStyle: TextStyle(
     424           0 :           color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
     425             :           fontFamily: "Inter",
     426             :           fontWeight: FontWeight.normal,
     427           0 :           fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
     428             :         ),
     429           0 :         codeStyle: TextStyle(
     430             :           // note: these colors are flipped
     431             :           fontWeight: FontWeight.normal,
     432           0 :           fontSize: 16.0 * Provider.of<Settings>(context).fontScaling,
     433           0 :           color: Provider.of<Settings>(context).theme.messageFromOtherTextColor,
     434           0 :           backgroundColor: Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
     435             :         ),
     436             :         textAlign: TextAlign.left,
     437             :         textWidthBasis: TextWidthBasis.longestLine,
     438             :         constraints: null,
     439             :       ),
     440             :     );
     441             : 
     442           0 :     var showMessageFormattingPreview = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
     443             :     var preview = showMessageFormattingPreview
     444           0 :         ? IconButton(
     445           0 :             tooltip: AppLocalizations.of(context)!.tooltipBackToMessageEditing,
     446           0 :             icon: Icon(Icons.text_fields),
     447           0 :             onPressed: () {
     448           0 :               setState(() {
     449           0 :                 showPreview = false;
     450             :               });
     451             :             },
     452             :           )
     453           0 :         : Container();
     454             : 
     455           0 :     var composeBox = Container(
     456           0 :       color: Provider.of<Settings>(context).theme.backgroundMainColor,
     457           0 :       padding: EdgeInsets.all(2),
     458           0 :       margin: EdgeInsets.all(2),
     459             : 
     460             :       // 164 minimum height + 16px for every line of text so the entire message is displayed when previewed.
     461           0 :       height: 164 + ((Provider.of<ContactInfoState>(context).messageDraft.messageText.split("\n").length - 1) * 16),
     462           0 :       child: Column(
     463           0 :         children: [
     464           0 :           Container(
     465           0 :             decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarBackgroundColor),
     466           0 :             child: Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [preview]),
     467             :           ),
     468           0 :           Container(
     469           0 :             decoration: BoxDecoration(
     470           0 :               border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor)),
     471             :             ),
     472           0 :             child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [wdgMessage]),
     473             :           ),
     474             :         ],
     475             :       ),
     476             :     );
     477           0 :     return Container(
     478           0 :       color: Provider.of<Settings>(context).theme.backgroundMainColor,
     479           0 :       child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]),
     480             :     );
     481             :   }
     482             : 
     483           0 :   Widget _buildComposeBox(BuildContext context) {
     484           0 :     bool cannotSend = Provider.of<ContactInfoState>(context).canSend() == false;
     485           0 :     bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
     486           0 :     var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
     487           0 :     var charLength = Provider.of<ContactInfoState>(context).messageDraft.messageText.characters.length;
     488           0 :     var expectedLength = Provider.of<ContactInfoState>(context).messageDraft.messageText.length;
     489           0 :     var numberOfBytesMoreThanChar = (expectedLength - charLength);
     490             : 
     491           0 :     var bold = IconButton(
     492           0 :       icon: Icon(Icons.format_bold),
     493           0 :       tooltip: AppLocalizations.of(context)!.tooltipBoldText,
     494           0 :       onPressed: () {
     495           0 :         setState(() {
     496           0 :           var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
     497           0 :           var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
     498           0 :           var selection = ctrlrCompose.selection;
     499           0 :           var start = ctrlrCompose.selection.start;
     500           0 :           var end = ctrlrCompose.selection.end;
     501           0 :           ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "**" + selected + "**");
     502           0 :           ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
     503           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
     504             :         });
     505             :       },
     506             :     );
     507             : 
     508           0 :     var italic = IconButton(
     509           0 :       icon: Icon(Icons.format_italic),
     510           0 :       tooltip: AppLocalizations.of(context)!.tooltipItalicize,
     511           0 :       onPressed: () {
     512           0 :         setState(() {
     513           0 :           var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
     514           0 :           var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
     515           0 :           var selection = ctrlrCompose.selection;
     516           0 :           var start = ctrlrCompose.selection.start;
     517           0 :           var end = ctrlrCompose.selection.end;
     518           0 :           ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "*" + selected + "*");
     519           0 :           ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
     520           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
     521             :         });
     522             :       },
     523             :     );
     524             : 
     525           0 :     var code = IconButton(
     526           0 :       icon: Icon(Icons.code),
     527           0 :       tooltip: AppLocalizations.of(context)!.tooltipCode,
     528           0 :       onPressed: () {
     529           0 :         setState(() {
     530           0 :           var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
     531           0 :           var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
     532           0 :           var selection = ctrlrCompose.selection;
     533           0 :           var start = ctrlrCompose.selection.start;
     534           0 :           var end = ctrlrCompose.selection.end;
     535           0 :           ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "`" + selected + "`");
     536           0 :           ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
     537           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
     538             :         });
     539             :       },
     540             :     );
     541             : 
     542           0 :     var superscript = IconButton(
     543           0 :       icon: Icon(Icons.superscript),
     544           0 :       tooltip: AppLocalizations.of(context)!.tooltipSuperscript,
     545           0 :       onPressed: () {
     546           0 :         setState(() {
     547           0 :           var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
     548           0 :           var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
     549           0 :           var selection = ctrlrCompose.selection;
     550           0 :           var start = ctrlrCompose.selection.start;
     551           0 :           var end = ctrlrCompose.selection.end;
     552           0 :           ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "^" + selected + "^");
     553           0 :           ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 1, extentOffset: selection.start + 1);
     554           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
     555             :         });
     556             :       },
     557             :     );
     558             : 
     559           0 :     var strikethrough = IconButton(
     560           0 :       icon: Icon(Icons.format_strikethrough),
     561           0 :       tooltip: AppLocalizations.of(context)!.tooltipStrikethrough,
     562           0 :       onPressed: () {
     563           0 :         setState(() {
     564           0 :           var ctrlrCompose = Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose;
     565           0 :           var selected = ctrlrCompose.selection.textInside(ctrlrCompose.text);
     566           0 :           var selection = ctrlrCompose.selection;
     567           0 :           var start = ctrlrCompose.selection.start;
     568           0 :           var end = ctrlrCompose.selection.end;
     569           0 :           ctrlrCompose.text = ctrlrCompose.text.replaceRange(start, end, "~~" + selected + "~~");
     570           0 :           ctrlrCompose.selection = selection.copyWith(baseOffset: selection.start + 2, extentOffset: selection.start + 2);
     571           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose = ctrlrCompose;
     572             :         });
     573             :       },
     574             :     );
     575             : 
     576           0 :     var preview = IconButton(
     577           0 :       icon: Icon(Icons.text_format),
     578           0 :       tooltip: AppLocalizations.of(context)!.tooltipPreviewFormatting,
     579           0 :       onPressed: () {
     580           0 :         setState(() {
     581           0 :           showPreview = true;
     582             :         });
     583             :       },
     584             :     );
     585             : 
     586           0 :     var vline = Padding(
     587           0 :       padding: EdgeInsets.symmetric(vertical: 1, horizontal: 2),
     588           0 :       child: Container(height: 16, width: 1, decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarIconColor)),
     589             :     );
     590             : 
     591           0 :     var toolbarButtons = [bold, italic, code, superscript, strikethrough, vline, preview];
     592             : 
     593           0 :     if (Provider.of<ContactInfoState>(context).isOnline()) {
     594           0 :       var expander = Expanded(child: Container());
     595           0 :       var sendInvite = IconButton(
     596           0 :         splashRadius: Material.defaultSplashRadius / 2,
     597           0 :         icon: Icon(CwtchIcons.send_invite, size: 18),
     598           0 :         tooltip: AppLocalizations.of(context)!.sendInvite,
     599           0 :         onPressed: () {
     600           0 :           _modalSendInvitation(context);
     601             :         },
     602             :       );
     603             : 
     604           0 :       toolbarButtons.addAll([expander, sendInvite]);
     605             : 
     606           0 :       if (Provider.of<Settings>(context).isExperimentEnabled(FileSharingExperiment)) {
     607           0 :         toolbarButtons.add(
     608           0 :           IconButton(
     609           0 :             splashRadius: Material.defaultSplashRadius / 2,
     610           0 :             icon: Icon(Icons.attach_file, size: 18, color: Provider.of<Settings>(context).theme.mainTextColor),
     611           0 :             tooltip: AppLocalizations.of(context)!.tooltipSendFile,
     612           0 :             onPressed: Provider.of<AppState>(context).disableFilePicker
     613             :                 ? null
     614           0 :                 : () {
     615           0 :                     imagePreview = null;
     616           0 :                     filesharing.showFilePicker(
     617             :                       context,
     618             :                       MaxGeneralFileSharingSize,
     619           0 :                       (File file) {
     620           0 :                         _confirmFileSend(context, file.path);
     621             :                       },
     622           0 :                       () {
     623           0 :                         final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.msgFileTooBig), duration: Duration(seconds: 4));
     624           0 :                         ScaffoldMessenger.of(context).showSnackBar(snackBar);
     625             :                       },
     626           0 :                       () {},
     627             :                     );
     628             :                   },
     629             :           ),
     630             :         );
     631             :       }
     632             :     }
     633             : 
     634           0 :     var formattingToolbar = Container(
     635           0 :       decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.toolbarBackgroundColor),
     636           0 :       child: Row(mainAxisAlignment: MainAxisAlignment.start, children: toolbarButtons),
     637             :     );
     638             : 
     639           0 :     var textField = Container(
     640           0 :       decoration: BoxDecoration(
     641           0 :         border: Border(top: BorderSide(color: Provider.of<Settings>(context).theme.backgroundHilightElementColor)),
     642             :       ),
     643           0 :       child: KeyboardListener(
     644           0 :         focusNode: FocusNode(),
     645           0 :         onKeyEvent: handleKeyPress,
     646           0 :         child: Padding(
     647           0 :           padding: EdgeInsets.all(8),
     648           0 :           child: TextFormField(
     649           0 :             key: Key('txtCompose'),
     650           0 :             controller: Provider.of<ContactInfoState>(context).messageDraft.ctrlCompose,
     651           0 :             focusNode: focusNode,
     652           0 :             autofocus: !Platform.isAndroid,
     653             :             textInputAction: TextInputAction.newline,
     654             :             textCapitalization: TextCapitalization.sentences,
     655             :             keyboardType: TextInputType.multiline,
     656             :             enableIMEPersonalizedLearning: false,
     657             :             minLines: 1,
     658           0 :             maxLength: max(1, (isGroup ? GroupMessageLengthMax : P2PMessageLengthMax) - numberOfBytesMoreThanChar),
     659             :             autocorrect: true,
     660           0 :             buildCounter: (context, {currentLength = 0, isFocused = true, maxLength}) {
     661           0 :               return Text("$currentLength/$maxLength", style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
     662             :             },
     663             :             maxLengthEnforcement: MaxLengthEnforcement.enforced,
     664             :             maxLines: 3,
     665           0 :             onFieldSubmitted: _sendMessage,
     666           0 :             style: Provider.of<Settings>(context).scaleFonts(defaultMessageTextStyle).copyWith(fontWeight: FontWeight.w500),
     667             :             enabled: true, // always allow editing...
     668             : 
     669           0 :             onChanged: (String x) {
     670           0 :               setState(() {
     671             :                 // we need to force a rerender here to update the max length count
     672             :               });
     673             :             },
     674           0 :             decoration: InputDecoration(
     675           0 :               hintText: AppLocalizations.of(context)!.placeholderEnterMessage,
     676           0 :               hintStyle: Provider.of<Settings>(context).scaleFonts(defaultMessageTextStyle).copyWith(color: Provider.of<Settings>(context).theme.sendHintTextColor, fontWeight: FontWeight.bold),
     677             :               enabledBorder: InputBorder.none,
     678             :               focusedBorder: InputBorder.none,
     679             :               enabled: true,
     680           0 :               suffixIcon: ElevatedButton(
     681           0 :                 key: Key("btnSend"),
     682           0 :                 style: ElevatedButton.styleFrom(
     683           0 :                   padding: EdgeInsets.all(0.0),
     684           0 :                   shape: new RoundedRectangleBorder(borderRadius: new BorderRadius.circular(45.0)),
     685             :                 ),
     686           0 :                 child: Tooltip(
     687             :                   message: cannotSend
     688           0 :                       ? (isGroup ? AppLocalizations.of(context)!.serverNotSynced : AppLocalizations.of(context)!.peerOfflineMessage)
     689           0 :                       : (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0)
     690           0 :                       ? AppLocalizations.of(context)!.acquiringTicketsFromServer
     691           0 :                       : AppLocalizations.of(context)!.sendMessage,
     692           0 :                   child: Icon(CwtchIcons.send_24px, size: 24, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
     693             :                 ),
     694           0 :                 onPressed: cannotSend || (isGroup && Provider.of<ContactInfoState>(context, listen: false).antispamTickets == 0) ? null : _sendMessage,
     695             :               ),
     696             :             ),
     697             :           ),
     698             :         ),
     699             :       ),
     700             :     );
     701             : 
     702             :     var textEditChildren;
     703             :     if (showToolbar) {
     704           0 :       textEditChildren = [formattingToolbar, textField];
     705             :     } else {
     706           0 :       textEditChildren = [textField];
     707             :     }
     708             : 
     709           0 :     var composeBox = Container(
     710           0 :       color: Provider.of<Settings>(context).theme.backgroundMainColor,
     711           0 :       padding: EdgeInsets.all(2),
     712           0 :       margin: EdgeInsets.all(2),
     713             :       height: 164,
     714           0 :       child: Column(children: textEditChildren),
     715             :     );
     716             : 
     717             :     var children;
     718           0 :     Widget invite = Container();
     719           0 :     if (Provider.of<ContactInfoState>(context).messageDraft.getInviteHandle() != null) {
     720           0 :       invite = FutureBuilder(
     721           0 :         future: Future.value(Provider.of<ContactInfoState>(context).messageDraft.getInviteHandle()),
     722           0 :         builder: (context, snapshot) {
     723           0 :           if (snapshot.hasData) {
     724           0 :             var contactInvite = snapshot.data! as int;
     725           0 :             var contact = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contactInvite);
     726           0 :             return Container(
     727           0 :               margin: EdgeInsets.all(5),
     728           0 :               padding: EdgeInsets.all(5),
     729           0 :               color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
     730           0 :               child: Column(
     731             :                 crossAxisAlignment: CrossAxisAlignment.start,
     732           0 :                 children: [
     733           0 :                   Stack(
     734           0 :                     children: [
     735           0 :                       Container(
     736           0 :                         margin: EdgeInsets.all(5),
     737           0 :                         padding: EdgeInsets.all(5),
     738             :                         clipBehavior: Clip.antiAlias,
     739           0 :                         decoration: BoxDecoration(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor),
     740             :                         height: 75,
     741           0 :                         child: Row(
     742             :                           mainAxisSize: MainAxisSize.max,
     743             :                           mainAxisAlignment: MainAxisAlignment.start,
     744           0 :                           children: [
     745           0 :                             Padding(padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Icon(CwtchIcons.send_invite, size: 32)),
     746           0 :                             Flexible(
     747           0 :                               child: DefaultTextStyle(
     748             :                                 textWidthBasis: TextWidthBasis.parent,
     749           0 :                                 child: senderInviteChrome("", contact!.nickname),
     750           0 :                                 style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle),
     751             :                                 overflow: TextOverflow.fade,
     752             :                               ),
     753             :                             ),
     754             :                           ],
     755             :                         ),
     756             :                       ),
     757           0 :                       Align(
     758             :                         alignment: Alignment.topRight,
     759           0 :                         child: IconButton(
     760           0 :                           icon: Icon(Icons.highlight_remove),
     761           0 :                           splashRadius: Material.defaultSplashRadius / 2,
     762           0 :                           color: Provider.of<Settings>(context).theme.messageFromMeTextColor,
     763           0 :                           tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
     764           0 :                           onPressed: () {
     765           0 :                             Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearInvite();
     766           0 :                             setState(() {});
     767             :                           },
     768             :                         ),
     769             :                       ),
     770             :                     ],
     771             :                   ),
     772             :                 ],
     773             :               ),
     774             :             );
     775             :           }
     776           0 :           return Container();
     777             :         },
     778             :       );
     779             :     }
     780             : 
     781           0 :     if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage() != null) {
     782           0 :       var quoted = FutureBuilder(
     783           0 :         future: messageHandler(
     784             :           context,
     785           0 :           Provider.of<AppState>(context).selectedProfile!,
     786           0 :           Provider.of<AppState>(context).selectedConversation!,
     787           0 :           ById(Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage()!.index),
     788             :         ),
     789           0 :         builder: (context, snapshot) {
     790           0 :           if (snapshot.hasData) {
     791           0 :             var message = snapshot.data! as Message;
     792           0 :             var qTextColor = message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
     793           0 :                 ? Provider.of<Settings>(context).theme.messageFromOtherTextColor
     794           0 :                 : Provider.of<Settings>(context).theme.messageFromMeTextColor;
     795           0 :             return Container(
     796           0 :               margin: EdgeInsets.all(5),
     797           0 :               padding: EdgeInsets.all(5),
     798           0 :               color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
     799           0 :                   ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
     800           0 :                   : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
     801           0 :               child: Column(
     802             :                 crossAxisAlignment: CrossAxisAlignment.start,
     803           0 :                 children: [
     804           0 :                   Stack(
     805           0 :                     children: [
     806           0 :                       Container(
     807           0 :                         margin: EdgeInsets.all(5),
     808           0 :                         padding: EdgeInsets.all(5),
     809             :                         clipBehavior: Clip.antiAlias,
     810           0 :                         decoration: BoxDecoration(
     811           0 :                           color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
     812           0 :                               ? Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor
     813           0 :                               : Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
     814             :                         ),
     815             :                         height: 75,
     816           0 :                         child: Row(
     817             :                           mainAxisSize: MainAxisSize.max,
     818             :                           mainAxisAlignment: MainAxisAlignment.start,
     819           0 :                           children: [
     820           0 :                             Padding(
     821           0 :                               padding: EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0),
     822           0 :                               child: Icon(Icons.reply, size: 32, color: qTextColor),
     823             :                             ),
     824           0 :                             Flexible(
     825           0 :                               child: DefaultTextStyle(
     826             :                                 textWidthBasis: TextWidthBasis.parent,
     827           0 :                                 child: message.getPreviewWidget(context),
     828           0 :                                 style: TextStyle(color: qTextColor),
     829             :                                 overflow: TextOverflow.fade,
     830             :                               ),
     831             :                             ),
     832             :                           ],
     833             :                         ),
     834             :                       ),
     835           0 :                       Align(
     836             :                         alignment: Alignment.topRight,
     837           0 :                         child: IconButton(
     838           0 :                           icon: Icon(Icons.highlight_remove),
     839           0 :                           splashRadius: Material.defaultSplashRadius / 2,
     840           0 :                           color: message.getMetadata().senderHandle != Provider.of<AppState>(context).selectedProfile
     841           0 :                               ? Provider.of<Settings>(context).theme.messageFromOtherTextColor
     842           0 :                               : Provider.of<Settings>(context).theme.messageFromMeTextColor,
     843           0 :                           tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
     844           0 :                           onPressed: () {
     845           0 :                             Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
     846           0 :                             setState(() {});
     847             :                           },
     848             :                         ),
     849             :                       ),
     850             :                     ],
     851             :                   ),
     852             :                 ],
     853             :               ),
     854             :             );
     855             :           } else {
     856           0 :             return MessageLoadingBubble();
     857             :           }
     858             :         },
     859             :       );
     860             : 
     861           0 :       children = [invite, quoted, composeBox];
     862             :     } else {
     863           0 :       children = [invite, composeBox];
     864             :     }
     865             : 
     866           0 :     return Container(
     867           0 :       color: Provider.of<Settings>(context).theme.backgroundMainColor,
     868           0 :       child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: children),
     869             :     );
     870             :   }
     871             : 
     872             :   // Send the message if enter is pressed without the shift key...
     873           0 :   void handleKeyPress(KeyEvent event) {
     874           0 :     var key = event.logicalKey;
     875           0 :     if ((event.logicalKey == LogicalKeyboardKey.enter && !HardwareKeyboard.instance.isShiftPressed) || event.logicalKey == LogicalKeyboardKey.numpadEnter && HardwareKeyboard.instance.isShiftPressed) {
     876             :       // Don't send when inserting a new line that is not at the end of the message
     877           0 :       if (Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose.selection.baseOffset !=
     878           0 :           Provider.of<ContactInfoState>(context, listen: false).messageDraft.ctrlCompose.text.length) {
     879             :         return;
     880             :       }
     881           0 :       _sendMessage();
     882           0 :       Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearDraft();
     883             :     }
     884             :   }
     885             : 
     886             :   // explicitly passing BuildContext ctx here is important, change at risk to own health
     887             :   // otherwise some Providers will become inaccessible to subwidgets...?
     888             :   // https://stackoverflow.com/a/63818697
     889           0 :   void _modalSendInvitation(BuildContext ctx) {
     890           0 :     showModalBottomSheet<void>(
     891             :       context: ctx,
     892           0 :       builder: (BuildContext bcontext) {
     893           0 :         return Container(
     894             :           height: 200, // bespoke value courtesy of the [TextField] docs
     895           0 :           child: Center(
     896           0 :             child: Padding(
     897           0 :               padding: EdgeInsets.all(10.0),
     898           0 :               child: Column(
     899             :                 mainAxisAlignment: MainAxisAlignment.center,
     900             :                 mainAxisSize: MainAxisSize.min,
     901             :                 crossAxisAlignment: CrossAxisAlignment.stretch,
     902           0 :                 children: <Widget>[
     903           0 :                   Text(AppLocalizations.of(bcontext)!.invitationLabel),
     904           0 :                   SizedBox(height: 20),
     905           0 :                   ChangeNotifierProvider.value(
     906           0 :                     value: Provider.of<ProfileInfoState>(ctx, listen: false),
     907           0 :                     child: DropdownContacts(
     908           0 :                       filter: (contact) {
     909           0 :                         return contact.onion != Provider.of<ContactInfoState>(ctx).onion;
     910             :                       },
     911           0 :                       onChanged: (newVal) {
     912           0 :                         setState(() {
     913           0 :                           this.selectedContact = Provider.of<ProfileInfoState>(ctx, listen: false).contactList.findContact(newVal)!.identifier;
     914             :                         });
     915             :                       },
     916             :                     ),
     917             :                   ),
     918           0 :                   SizedBox(height: 20),
     919           0 :                   ElevatedButton(
     920           0 :                     child: Text(AppLocalizations.of(bcontext)!.inviteBtn, semanticsLabel: AppLocalizations.of(bcontext)!.inviteBtn),
     921           0 :                     onPressed: () {
     922           0 :                       if (this.selectedContact != -1) {
     923           0 :                         Provider.of<ContactInfoState>(context, listen: false).messageDraft.attachInvite(this.selectedContact);
     924             :                       }
     925           0 :                       Navigator.pop(bcontext);
     926           0 :                       setState(() {});
     927             :                     },
     928             :                   ),
     929             :                 ],
     930             :               ),
     931             :             ),
     932             :           ),
     933             :         );
     934             :       },
     935             :     );
     936             :   }
     937             : 
     938           0 :   void _confirmFileSend(BuildContext ctx, String path) async {
     939           0 :     showModalBottomSheet<void>(
     940             :       context: ctx,
     941           0 :       builder: (BuildContext bcontext) {
     942             :         var showPreview = false;
     943           0 :         if (Provider.of<Settings>(context, listen: false).shouldPreview(path)) {
     944             :           showPreview = true;
     945           0 :           if (imagePreview == null) {
     946           0 :             imagePreview = new File(path);
     947             :           }
     948             :         }
     949           0 :         return Container(
     950             :           height: 300, // bespoke value courtesy of the [TextField] docs
     951           0 :           child: Center(
     952           0 :             child: Padding(
     953           0 :               padding: EdgeInsets.all(10.0),
     954           0 :               child: Column(
     955             :                 mainAxisAlignment: MainAxisAlignment.center,
     956             :                 mainAxisSize: MainAxisSize.min,
     957           0 :                 children: <Widget>[
     958           0 :                   Text(AppLocalizations.of(context)!.msgConfirmSend + " $path?"),
     959           0 :                   SizedBox(height: 20),
     960           0 :                   Visibility(
     961             :                     visible: showPreview,
     962             :                     child: showPreview
     963           0 :                         ? Image.file(
     964           0 :                             imagePreview!,
     965             :                             cacheHeight: 150, // limit the amount of space the image can decode too, we keep this high-ish to allow quality previews...
     966             :                             filterQuality: FilterQuality.medium,
     967             :                             fit: BoxFit.fill,
     968             :                             alignment: Alignment.center,
     969             :                             height: 150,
     970             :                             isAntiAlias: false,
     971           0 :                             errorBuilder: (context, error, stackTrace) {
     972           0 :                               return MalformedBubble();
     973             :                             },
     974             :                           )
     975           0 :                         : Container(),
     976             :                   ),
     977           0 :                   Visibility(visible: showPreview, child: SizedBox(height: 10)),
     978           0 :                   Row(
     979             :                     mainAxisAlignment: MainAxisAlignment.center,
     980           0 :                     children: [
     981           0 :                       OutlinedButton(
     982           0 :                         child: Text(AppLocalizations.of(context)!.cancel, semanticsLabel: AppLocalizations.of(context)!.cancel),
     983           0 :                         onPressed: () {
     984           0 :                           Navigator.pop(bcontext);
     985             :                         },
     986             :                       ),
     987           0 :                       SizedBox(width: 20),
     988           0 :                       FilledButton(
     989           0 :                         child: Text(AppLocalizations.of(context)!.btnSendFile, semanticsLabel: AppLocalizations.of(context)!.btnSendFile),
     990           0 :                         onPressed: () {
     991           0 :                           _sendFile(path);
     992           0 :                           Navigator.pop(bcontext);
     993             :                         },
     994             :                       ),
     995             :                     ],
     996             :                   ),
     997             :                 ],
     998             :               ),
     999             :             ),
    1000             :           ),
    1001             :         );
    1002             :       },
    1003             :     );
    1004             :   }
    1005             : }
    1006             : 
    1007           0 : void _summarizeConversation(BuildContext context, ProfileInfoState profile, Settings settings) async {
    1008           0 :   showModalBottomSheet<void>(
    1009           0 :     builder: (BuildContext bcontext) {
    1010           0 :       return StatefulBuilder(
    1011           0 :         builder: (BuildContext scontext, StateSetter setState /*You can rename this!*/) {
    1012           0 :           if (scontext.mounted) {
    1013           0 :             new Timer.periodic(Duration(seconds: 1), (Timer t) {
    1014           0 :               if (scontext.mounted) {
    1015           0 :                 setState(() {});
    1016             :               }
    1017             :             });
    1018             :           }
    1019             : 
    1020           0 :           var bubble = StaticMessageBubble(
    1021             :             profile,
    1022             :             settings,
    1023           0 :             MessageMetadata(profile.onion, Provider.of<ContactInfoState>(context).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""),
    1024           0 :             Row(
    1025           0 :               children: [
    1026           0 :                 Provider.of<ContactInfoState>(context).summary == ""
    1027           0 :                     ? Column(
    1028             :                         crossAxisAlignment: CrossAxisAlignment.center,
    1029           0 :                         children: [
    1030           0 :                           CircularProgressIndicator(color: settings.theme.defaultButtonActiveColor),
    1031           0 :                           Padding(padding: EdgeInsets.all(5.0), child: Text(AppLocalizations.of(context)!.blodeuweddProcessing)),
    1032             :                         ],
    1033             :                       )
    1034           0 :                     : Flexible(child: Text(Provider.of<ContactInfoState>(context).summary)),
    1035             :               ],
    1036             :             ),
    1037             :           );
    1038             : 
    1039           0 :           var image = Padding(
    1040           0 :             padding: EdgeInsets.all(4.0),
    1041           0 :             child: ProfileImage(imagePath: "assets/blodeuwedd.png", diameter: 48.0, border: settings.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
    1042             :           );
    1043             : 
    1044           0 :           return Container(
    1045             :             height: 300, // bespoke value courtesy of the [TextField] docs
    1046           0 :             child: Container(
    1047             :               alignment: Alignment.center,
    1048           0 :               child: Padding(
    1049           0 :                 padding: EdgeInsets.all(10.0),
    1050           0 :                 child: Padding(
    1051           0 :                   padding: EdgeInsets.all(10.0),
    1052           0 :                   child: Row(
    1053             :                     mainAxisSize: MainAxisSize.min,
    1054           0 :                     children: [
    1055             :                       image,
    1056           0 :                       Flexible(child: bubble),
    1057             :                     ],
    1058             :                   ),
    1059             :                 ),
    1060             :               ),
    1061             :             ),
    1062             :           );
    1063             :         },
    1064             :       );
    1065             :     },
    1066             :     context: context,
    1067             :   );
    1068             : }

Generated by: LCOV version 1.14