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

Generated by: LCOV version 1.14