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

Generated by: LCOV version 1.14