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

Generated by: LCOV version 1.14