LCOV - code coverage report
Current view: top level - lib/widgets - messagerow.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 309 0.0 %
Date: 2026-08-30 05:23:47 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:io';
       3             : 
       4             : import 'package:cwtch/config.dart';
       5             : import 'package:cwtch/cwtch_icons_icons.dart';
       6             : import 'package:cwtch/models/contact.dart';
       7             : import 'package:cwtch/models/message.dart';
       8             : import 'package:cwtch/models/profile.dart';
       9             : import 'package:cwtch/themes/opaque.dart';
      10             : import 'package:cwtch/third_party/base32/base32.dart';
      11             : import 'package:cwtch/views/contactsview.dart';
      12             : import 'package:cwtch/widgets/deletedbubble.dart';
      13             : import 'package:cwtch/widgets/eventbubble.dart';
      14             : import 'package:cwtch/widgets/messageloadingbubble.dart';
      15             : import 'package:cwtch/widgets/staticmessagebubble.dart';
      16             : import 'package:flutter/material.dart';
      17             : import 'package:cwtch/widgets/profileimage.dart';
      18             : import 'package:flutter/physics.dart';
      19             : import 'package:provider/provider.dart';
      20             : import 'package:cwtch/l10n/app_localizations.dart';
      21             : 
      22             : import '../main.dart';
      23             : import '../models/messagecache.dart';
      24             : import '../models/redaction.dart';
      25             : import '../settings.dart';
      26             : 
      27             : class MessageRow extends StatefulWidget {
      28             :   final Widget child;
      29             :   final int index;
      30             : 
      31           0 :   MessageRow(this.child, this.index, {Key? key}) : super(key: key);
      32             : 
      33           0 :   @override
      34           0 :   MessageRowState createState() => MessageRowState();
      35             : }
      36             : 
      37             : class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMixin {
      38             :   bool showBlockedMessage = false;
      39             :   late AnimationController _controller;
      40             :   late Animation<Alignment> _animation;
      41             :   late Alignment _dragAlignment = Alignment.center;
      42             :   Alignment _dragAffinity = Alignment.center;
      43             : 
      44           0 :   @override
      45             :   void initState() {
      46           0 :     super.initState();
      47           0 :     _controller = AnimationController(vsync: this);
      48           0 :     _controller.addListener(() {
      49           0 :       setState(() {
      50           0 :         _dragAlignment = _animation.value;
      51             :       });
      52             :     });
      53             :   }
      54             : 
      55           0 :   @override
      56             :   void dispose() {
      57           0 :     _controller.dispose();
      58           0 :     super.dispose();
      59             :   }
      60             : 
      61           0 :   @override
      62             :   Widget build(BuildContext context) {
      63             :     // message cache will return a malformed metadata object
      64             :     // if for whatever reason this message doesn't exist
      65           0 :     if (Provider.of<MessageMetadata>(context).senderHandle == "") {
      66           0 :       EnvironmentConfig.debugLog("error, cache returned malformed message. This is likely a programming bug.");
      67           0 :       return MessageLoadingBubble();
      68             :     }
      69           0 :     var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
      70           0 :     var isContact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
      71           0 :     var isGroup = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context, listen: false).conversationIdentifier)!.isGroup;
      72           0 :     var isBlocked = isContact ? Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle)!.isBlocked : false;
      73           0 :     var actualMessage = Flexible(flex: Platform.isAndroid ? 10 : 100, fit: FlexFit.loose, child: widget.child);
      74           0 :     bool pending = Provider.of<ContactInfoState>(context).isOpPending;
      75             : 
      76           0 :     _dragAffinity = fromMe ? Alignment.centerRight : Alignment.centerLeft;
      77           0 :     _dragAlignment = fromMe ? Alignment.centerRight : Alignment.centerLeft;
      78             : 
      79             :     var senderDisplayStr = "";
      80             :     if (!fromMe) {
      81           0 :       ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
      82             :       if (contact != null) {
      83           0 :         senderDisplayStr = redactedNick(context, contact.onion, contact.nickname);
      84             :       } else {
      85           0 :         senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
      86             :       }
      87             :     }
      88             : 
      89           0 :     Widget wdgReply = Platform.isAndroid
      90           0 :         ? SizedBox.shrink()
      91           0 :         : Visibility(
      92           0 :             visible: EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
      93             :             maintainSize: true,
      94             :             maintainAnimation: true,
      95             :             maintainState: true,
      96             :             maintainInteractivity: false,
      97           0 :             child: IconButton(
      98           0 :               tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
      99           0 :               splashRadius: Material.defaultSplashRadius / 2,
     100           0 :               onPressed: () {
     101           0 :                 Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
     102           0 :                 Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
     103           0 :                 setState(() {});
     104             :               },
     105           0 :               icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
     106             :             ),
     107             :           );
     108             : 
     109           0 :     var settings = Provider.of<Settings>(context);
     110           0 :     var pis = Provider.of<ProfileInfoState>(context);
     111           0 :     var cis = Provider.of<ContactInfoState>(context);
     112           0 :     var borderColor = Provider.of<Settings>(context).theme.portraitOnlineBorderColor;
     113           0 :     var messageID = Provider.of<MessageMetadata>(context).messageID;
     114           0 :     var cache = Provider.of<ContactInfoState>(context).messageCache;
     115             : 
     116           0 :     Widget wdgSeeReplies = Platform.isAndroid
     117           0 :         ? SizedBox.shrink()
     118           0 :         : Visibility(
     119           0 :             visible: EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID,
     120             :             maintainSize: true,
     121             :             maintainAnimation: true,
     122             :             maintainState: true,
     123             :             maintainInteractivity: false,
     124           0 :             child: IconButton(
     125           0 :               tooltip: AppLocalizations.of(context)!.viewReplies,
     126           0 :               splashRadius: Material.defaultSplashRadius / 2,
     127           0 :               onPressed: () {
     128           0 :                 modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
     129             :               },
     130           0 :               icon: Icon(CwtchIcons.view_replies, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
     131             :             ),
     132             :           );
     133             : 
     134           0 :     var profile = Provider.of<ProfileInfoState>(context, listen: false);
     135           0 :     var conversation = Provider.of<ContactInfoState>(context, listen: false);
     136           0 :     var message = Provider.of<MessageMetadata>(context, listen: false);
     137             : 
     138           0 :     Widget wdgTranslateMessage = Platform.isAndroid
     139           0 :         ? SizedBox.shrink()
     140           0 :         : Visibility(
     141             :             visible:
     142           0 :                 Provider.of<FlwtchState>(context, listen: false).cwtch.IsBlodeuweddSupported() &&
     143           0 :                 Provider.of<Settings>(context).isExperimentEnabled(BlodeuweddExperiment) &&
     144           0 :                 (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
     145             :             maintainSize: true,
     146             :             maintainAnimation: true,
     147             :             maintainState: true,
     148             :             maintainInteractivity: false,
     149           0 :             child: IconButton(
     150           0 :               tooltip: AppLocalizations.of(context)!.blodeuweddTranslate,
     151           0 :               splashRadius: Material.defaultSplashRadius / 2,
     152           0 :               onPressed: () {
     153           0 :                 Provider.of<MessageMetadata>(context, listen: false).translation = "";
     154           0 :                 Provider.of<FlwtchState>(context, listen: false).cwtch.TranslateMessage(profile.onion, conversation.identifier, message.messageID, "French");
     155           0 :                 modalShowTranslation(context, profile, settings);
     156             :               },
     157           0 :               icon: Icon(Icons.translate, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
     158             :             ),
     159             :           );
     160             : 
     161           0 :     bool isHidden = Provider.of<MessageMetadata>(context).attributes["hidden"] == "true";
     162           0 :     bool isWithheld = Provider.of<MessageMetadata>(context).attributes["withheld"] == "true";
     163           0 :     bool canMod = conversation.isManaged && conversation.isOnline() && ['admin', 'op', 'mod'].contains(conversation.getRole(profile.onion));
     164           0 :     bool isDeleted = widget.child.runtimeType == DeletedBubble;
     165             : 
     166           0 :     Widget wdgHideMessage = (!canMod || isDeleted || Platform.isAndroid)
     167           0 :         ? SizedBox.shrink()
     168           0 :         : Visibility(
     169             :             visible:
     170             :                 //TODO: experiment enabled?
     171           0 :                 (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
     172             :             maintainSize: true,
     173             :             maintainAnimation: true,
     174             :             maintainState: true,
     175             :             maintainInteractivity: false,
     176           0 :             child: IconButton(
     177             :               tooltip: isWithheld ? "t9n(Reveal)" : (isHidden ? "t9n(unhide message)" : "t9n(hide message)"),
     178           0 :               splashRadius: Material.defaultSplashRadius / 2,
     179             :               onPressed: pending
     180             :                   ? null
     181           0 :                   : () {
     182           0 :                       if (message.signature == null || message.signature!.isEmpty) {
     183             :                         return;
     184             :                       }
     185           0 :                       Provider.of<ContactInfoState>(context, listen: false).isOpPending = true;
     186           0 :                       Provider.of<FlwtchState>(context, listen: false).cwtch.ModerateMessage(profile.onion, conversation.identifier, message.signature!, isWithheld ? "R" : (isHidden ? "U" : "H"));
     187             :                     },
     188           0 :               icon: Icon((isWithheld || isHidden) ? CwtchIcons.eye_open : CwtchIcons.eye_closed, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
     189             :             ),
     190             :           );
     191             : 
     192           0 :     Widget wdgDeleteMessage = (!canMod || isDeleted || Platform.isAndroid)
     193           0 :         ? SizedBox.shrink()
     194           0 :         : Visibility(
     195             :             visible:
     196             :                 //TODO: experiment enabled?
     197           0 :                 (EnvironmentConfig.TEST_MODE || Provider.of<ContactInfoState>(context).hoveredIndex == Provider.of<MessageMetadata>(context).messageID),
     198             :             maintainSize: true,
     199             :             maintainAnimation: true,
     200             :             maintainState: true,
     201             :             maintainInteractivity: false,
     202           0 :             child: IconButton(
     203             :               tooltip: "t9n(delete message)",
     204           0 :               splashRadius: Material.defaultSplashRadius / 2,
     205             :               onPressed: pending
     206             :                   ? null
     207           0 :                   : () {
     208           0 :                       if (message.signature == null || message.signature!.isEmpty) {
     209             :                         return;
     210             :                       }
     211           0 :                       Provider.of<ContactInfoState>(context, listen: false).isOpPending = true;
     212           0 :                       Provider.of<FlwtchState>(context, listen: false).cwtch.ModerateMessage(profile.onion, conversation.identifier, message.signature!, "D");
     213             :                     },
     214           0 :               icon: Icon(CwtchIcons.delete_24px, color: Provider.of<Settings>(context).theme.chatReactionIconColor),
     215             :             ),
     216             :           );
     217             : 
     218           0 :     Widget wdgSpacer = Flexible(flex: 1, child: SizedBox(width: Platform.isAndroid ? 20 : 60, height: 10));
     219           0 :     var widgetRow = <Widget>[];
     220             : 
     221           0 :     if (widget.child.runtimeType == EventBubble) {
     222           0 :       widgetRow = <Widget>[wdgSpacer, actualMessage];
     223             :     } else if (fromMe) {
     224           0 :       widgetRow = <Widget>[wdgSpacer, wdgTranslateMessage, wdgHideMessage, wdgDeleteMessage, wdgSeeReplies, wdgReply, actualMessage];
     225           0 :     } else if (isBlocked && !showBlockedMessage) {
     226           0 :       Color blockedMessageBackground = Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor;
     227           0 :       Widget wdgPortrait = Padding(padding: EdgeInsets.all(4.0), child: Icon(CwtchIcons.account_blocked));
     228           0 :       widgetRow = <Widget>[
     229             :         wdgPortrait,
     230           0 :         Container(
     231           0 :           padding: EdgeInsets.all(2.0),
     232           0 :           decoration: BoxDecoration(
     233             :             color: blockedMessageBackground,
     234           0 :             border: Border.all(color: blockedMessageBackground, width: 2),
     235           0 :             borderRadius: BorderRadius.only(topLeft: Radius.circular(15.0), topRight: Radius.circular(15.0), bottomLeft: Radius.circular(15.0), bottomRight: Radius.circular(15.0)),
     236             :           ),
     237           0 :           child: Padding(
     238           0 :             padding: EdgeInsets.all(9.0),
     239           0 :             child: Row(
     240             :               crossAxisAlignment: CrossAxisAlignment.center,
     241           0 :               children: [
     242           0 :                 SelectableText(
     243           0 :                   AppLocalizations.of(context)!.blockedMessageMessage,
     244             :                   //key: Key(myKey),
     245           0 :                   style: TextStyle(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor),
     246             :                   textAlign: TextAlign.center,
     247             :                   textWidthBasis: TextWidthBasis.longestLine,
     248             :                 ),
     249           0 :                 IconButton(
     250             :                   enableFeedback: true,
     251           0 :                   splashRadius: Material.defaultSplashRadius / 2,
     252           0 :                   tooltip: AppLocalizations.of(context)!.showMessageButton,
     253           0 :                   icon: Icon(Icons.preview, color: Provider.of<Settings>(context).current().mainTextColor),
     254           0 :                   onPressed: () {
     255           0 :                     setState(() {
     256           0 :                       this.showBlockedMessage = true;
     257             :                     });
     258             :                   },
     259             :                 ),
     260             :               ],
     261             :             ),
     262             :           ),
     263             :         ),
     264             :         wdgSpacer,
     265             :       ];
     266             :     } else {
     267           0 :       var contact = Provider.of<ContactInfoState>(context);
     268           0 :       ContactInfoState? sender = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
     269             : 
     270           0 :       String imagePath = Provider.of<MessageMetadata>(context).senderImage!;
     271             :       if (sender != null) {
     272           0 :         imagePath = Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath;
     273             :       } else {
     274           0 :         imagePath = RandomProfileImage(Provider.of<MessageMetadata>(context).senderHandle);
     275             :       }
     276           0 :       Widget wdgPortrait = GestureDetector(
     277             :         onTap: !isGroup
     278             :             ? null
     279             :             : isContact
     280           0 :             ? _btnGoto
     281           0 :             : _btnAdd,
     282           0 :         child: Padding(
     283           0 :           padding: EdgeInsets.all(4.0),
     284           0 :           child: ProfileImage(
     285             :             diameter: 48.0,
     286             :             // default to the contact image...otherwise use a derived sender image...
     287             :             imagePath: imagePath,
     288           0 :             border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor,
     289             :             badgeTextColor: Colors.red,
     290             :             badgeColor: Colors.red,
     291             :             tooltip: !isGroup
     292             :                 ? ""
     293             :                 : isContact
     294           0 :                 ? AppLocalizations.of(context)!.contactGoto.replaceFirst("%1", senderDisplayStr)
     295           0 :                 : AppLocalizations.of(context)!.addContact,
     296             :           ),
     297             :         ),
     298             :       );
     299             : 
     300           0 :       widgetRow = <Widget>[wdgPortrait, actualMessage, wdgReply, wdgSeeReplies, wdgHideMessage, wdgDeleteMessage, wdgTranslateMessage, wdgSpacer];
     301             :     }
     302           0 :     var size = MediaQuery.of(context).size;
     303           0 :     var mr = MouseRegion(
     304             :       // For desktop...
     305           0 :       onHover: (event) {
     306           0 :         if (Provider.of<ContactInfoState>(context, listen: false).hoveredIndex != Provider.of<MessageMetadata>(context, listen: false).messageID) {
     307           0 :           Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
     308             :         }
     309             :       },
     310           0 :       onExit: (event) {
     311           0 :         Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = -1;
     312             :       },
     313           0 :       child: GestureDetector(
     314           0 :         onPanUpdate: (details) {
     315           0 :           setState(() {
     316           0 :             _dragAlignment += Alignment(details.delta.dx / (size.width * 0.5), 0);
     317             :           });
     318             :         },
     319           0 :         onPanDown: (details) {
     320           0 :           _controller.stop();
     321             :         },
     322           0 :         onPanEnd: (details) {
     323           0 :           _runAnimation(details.velocity.pixelsPerSecond, size);
     324           0 :           if (Platform.isAndroid) {
     325           0 :             Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
     326           0 :             Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
     327           0 :             setState(() {});
     328             :           }
     329             :         },
     330           0 :         onLongPress: () async {
     331           0 :           if (Platform.isAndroid) {
     332           0 :             modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
     333             :           }
     334             :         },
     335           0 :         child: Padding(
     336           0 :           padding: EdgeInsets.all(2),
     337           0 :           child: Align(
     338             :             widthFactor: 1,
     339           0 :             alignment: _dragAlignment,
     340           0 :             child: Row(mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: widgetRow),
     341             :           ),
     342             :         ),
     343             :       ),
     344             :     );
     345             : 
     346           0 :     if (Provider.of<ContactInfoState>(context).newMarkerMsgIndex == widget.index) {
     347           0 :       return Column(
     348             :         crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
     349           0 :         children: [
     350           0 :           Align(
     351             :             alignment: Alignment.center,
     352           0 :             child: Padding(padding: EdgeInsets.all(5.0), child: _bubbleNew()),
     353             :           ),
     354             :           mr,
     355             :         ],
     356             :       );
     357             :     } else {
     358             :       return mr;
     359             :     }
     360             :   }
     361             : 
     362           0 :   Widget _bubbleNew() {
     363           0 :     return Container(
     364           0 :       decoration: BoxDecoration(
     365           0 :         color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor,
     366           0 :         border: Border.all(color: Provider.of<Settings>(context).theme.messageFromMeBackgroundColor, width: 1),
     367           0 :         borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8), bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8)),
     368             :       ),
     369           0 :       child: Padding(
     370           0 :         padding: EdgeInsets.all(9.0),
     371           0 :         child: Text(AppLocalizations.of(context)!.newMessagesLabel, style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
     372             :       ),
     373             :     );
     374             :   }
     375             : 
     376           0 :   void _runAnimation(Offset pixelsPerSecond, Size size) {
     377           0 :     _animation = _controller.drive(AlignmentTween(begin: _dragAlignment, end: _dragAffinity));
     378             :     // Calculate the velocity relative to the unit interval, [0,1],
     379             :     // used by the animation controller.
     380           0 :     final unitsPerSecondX = pixelsPerSecond.dx / size.width;
     381           0 :     final unitsPerSecondY = pixelsPerSecond.dy / size.height;
     382           0 :     final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
     383           0 :     final unitVelocity = unitsPerSecond.distance;
     384             : 
     385             :     const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);
     386             : 
     387           0 :     final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
     388           0 :     _controller.animateWith(simulation);
     389             :   }
     390             : 
     391           0 :   void _btnGoto() {
     392           0 :     var id = Provider.of<ProfileInfoState>(context, listen: false).contactList.findContact(Provider.of<MessageMetadata>(context, listen: false).senderHandle)?.identifier;
     393             :     if (id == null) {
     394             :       // Can't happen
     395             :     } else {
     396           0 :       selectConversation(context, id, null);
     397           0 :       var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).filteredList().indexWhere((element) => element.identifier == id);
     398           0 :       Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.jumpTo(index: contactIndex);
     399             :     }
     400             :   }
     401             : 
     402           0 :   void _btnAdd() {
     403           0 :     var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
     404           0 :     if (sender == "") {
     405           0 :       print("sender not yet loaded");
     406             :       return;
     407             :     }
     408           0 :     var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
     409             : 
     410           0 :     showAddContactConfirmAlertDialog(context, profileOnion, sender);
     411             :   }
     412             : 
     413           0 :   showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
     414             :     // set up the buttons
     415           0 :     Widget cancelButton = ElevatedButton(
     416           0 :       child: Text(AppLocalizations.of(context)!.cancel),
     417           0 :       style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
     418           0 :       onPressed: () {
     419           0 :         Navigator.of(context).pop(); // dismiss dialog
     420             :       },
     421             :     );
     422           0 :     Widget continueButton = ElevatedButton(
     423           0 :       style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
     424           0 :       child: Text(AppLocalizations.of(context)!.addContact),
     425           0 :       onPressed: () {
     426           0 :         Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, senderOnion);
     427           0 :         final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullAddedContact), duration: Duration(seconds: 2));
     428           0 :         ScaffoldMessenger.of(context).showSnackBar(snackBar);
     429           0 :         Navigator.of(context).pop(); // dismiss dialog
     430             :       },
     431             :     );
     432             : 
     433             :     // set up the AlertDialog
     434           0 :     AlertDialog alert = AlertDialog(title: Text(AppLocalizations.of(context)!.addContactConfirm.replaceFirst("%1", senderOnion)), actions: [cancelButton, continueButton]);
     435             : 
     436             :     // show the dialog
     437           0 :     showDialog(
     438             :       context: context,
     439           0 :       builder: (BuildContext context) {
     440             :         return alert;
     441             :       },
     442             :     );
     443             :   }
     444             : }
     445             : 
     446           0 : void modalShowReplies(
     447             :   BuildContext ctx,
     448             :   String replyHeader,
     449             :   String noRepliesText,
     450             :   Settings settings,
     451             :   ProfileInfoState profile,
     452             :   ContactInfoState cis,
     453             :   Color borderColor,
     454             :   MessageCache cache,
     455             :   int messageID, {
     456             :   bool showImage = true,
     457             : }) {
     458           0 :   showModalBottomSheet<void>(
     459             :     context: ctx,
     460           0 :     builder: (BuildContext bcontext) {
     461           0 :       List<Message> replies = getReplies(cache, messageID);
     462           0 :       ScrollController controller = ScrollController();
     463             : 
     464           0 :       return MultiProvider(
     465           0 :         providers: [
     466           0 :           ChangeNotifierProvider.value(value: Provider.of<ContactInfoState>(ctx)),
     467           0 :           ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(ctx)),
     468             :         ],
     469           0 :         child: LayoutBuilder(
     470           0 :           builder: (BuildContext context, BoxConstraints viewportConstraints) {
     471           0 :             var replyWidgets = replies.map((e) {
     472           0 :               var fromMe = e.getMetadata().senderHandle == profile.onion;
     473             : 
     474           0 :               var bubble = StaticMessageBubble(profile, settings, e.getMetadata(), Row(children: [Flexible(child: e.getPreviewWidget(context))]));
     475             : 
     476           0 :               String imagePath = e.getMetadata().senderImage!;
     477           0 :               var sender = profile.contactList.findContact(e.getMetadata().senderHandle);
     478             :               if (sender != null) {
     479           0 :                 imagePath = showImage ? sender.imagePath : sender.defaultImagePath;
     480             :               } else {
     481           0 :                 imagePath = RandomProfileImage(e.getMetadata().senderHandle);
     482             :               }
     483             : 
     484             :               if (fromMe) {
     485           0 :                 imagePath = profile.imagePath;
     486             :               }
     487             : 
     488           0 :               var image = Padding(
     489           0 :                 padding: EdgeInsets.all(4.0),
     490           0 :                 child: ProfileImage(imagePath: imagePath, diameter: 48.0, border: borderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
     491             :               );
     492             : 
     493           0 :               return Padding(
     494           0 :                 padding: EdgeInsets.all(10.0),
     495           0 :                 child: Row(
     496             :                   mainAxisSize: MainAxisSize.min,
     497           0 :                   children: [
     498             :                     image,
     499           0 :                     Flexible(child: bubble),
     500             :                   ],
     501             :                 ),
     502             :               );
     503           0 :             }).toList();
     504             : 
     505             :             var withHeader = replyWidgets;
     506             : 
     507           0 :             var original = StaticMessageBubble(
     508             :               profile,
     509             :               settings,
     510           0 :               cache.cache[messageID]!.metadata,
     511           0 :               Row(children: [Flexible(child: compileOverlay(cache.cache[messageID]!).getPreviewWidget(context))]),
     512             :             );
     513             : 
     514           0 :             withHeader.insert(
     515             :               0,
     516           0 :               Padding(
     517           0 :                 padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
     518           0 :                 child: Center(child: original),
     519             :               ),
     520             :             );
     521             : 
     522           0 :             withHeader.insert(
     523             :               1,
     524           0 :               Padding(
     525           0 :                 padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
     526           0 :                 child: Divider(color: settings.theme.mainTextColor),
     527             :               ),
     528             :             );
     529             : 
     530           0 :             if (replies.isNotEmpty) {
     531           0 :               withHeader.insert(
     532             :                 2,
     533           0 :                 Padding(
     534           0 :                   padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
     535           0 :                   child: Text(replyHeader, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
     536             :                 ),
     537             :               );
     538             :             } else {
     539           0 :               withHeader.insert(
     540             :                 2,
     541           0 :                 Padding(
     542           0 :                   padding: EdgeInsets.fromLTRB(10.0, 10.0, 2.0, 15.0),
     543           0 :                   child: Center(
     544           0 :                     child: Text(noRepliesText, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
     545             :                   ),
     546             :                 ),
     547             :               );
     548             :             }
     549             : 
     550           0 :             return Scrollbar(
     551             :               trackVisibility: true,
     552             :               controller: controller,
     553           0 :               child: SingleChildScrollView(
     554             :                 clipBehavior: Clip.antiAlias,
     555           0 :                 child: ConstrainedBox(
     556           0 :                   constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight),
     557           0 :                   child: Padding(
     558           0 :                     padding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 20.0),
     559           0 :                     child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: withHeader),
     560             :                   ),
     561             :                 ),
     562             :               ),
     563             :             );
     564             :           },
     565             :         ),
     566             :       );
     567             :     },
     568             :   );
     569             : }
     570             : 
     571           0 : void modalShowTranslation(BuildContext context, ProfileInfoState profile, Settings settings) async {
     572           0 :   showModalBottomSheet<void>(
     573           0 :     builder: (BuildContext bcontext) {
     574           0 :       return StatefulBuilder(
     575           0 :         builder: (BuildContext scontext, StateSetter setState /*You can rename this!*/) {
     576           0 :           if (scontext.mounted) {
     577           0 :             new Timer.periodic(Duration(seconds: 1), (Timer t) {
     578           0 :               if (scontext.mounted) {
     579           0 :                 setState(() {});
     580             :               }
     581             :             });
     582             :           }
     583             : 
     584           0 :           var bubble = StaticMessageBubble(
     585             :             profile,
     586             :             settings,
     587           0 :             MessageMetadata(profile.onion, Provider.of<ContactInfoState>(context, listen: false).identifier, 1, DateTime.now(), "blodeuwedd", null, null, null, true, false, false, ""),
     588           0 :             Row(
     589           0 :               children: [
     590           0 :                 Provider.of<MessageMetadata>(context).translation == ""
     591           0 :                     ? Column(
     592             :                         crossAxisAlignment: CrossAxisAlignment.center,
     593           0 :                         children: [
     594           0 :                           CircularProgressIndicator(color: settings.theme.defaultButtonActiveColor),
     595           0 :                           Padding(padding: EdgeInsets.all(5.0), child: Text(AppLocalizations.of(context)!.blodeuweddProcessing)),
     596             :                         ],
     597             :                       )
     598           0 :                     : Flexible(child: SelectableText(Provider.of<MessageMetadata>(context).translation)),
     599             :               ],
     600             :             ),
     601             :           );
     602             : 
     603           0 :           var image = Padding(
     604           0 :             padding: EdgeInsets.all(4.0),
     605           0 :             child: ProfileImage(imagePath: "assets/blodeuwedd.png", diameter: 48.0, border: settings.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red),
     606             :           );
     607             : 
     608           0 :           return Container(
     609             :             height: 300, // bespoke value courtesy of the [TextField] docs
     610           0 :             child: Container(
     611             :               alignment: Alignment.center,
     612           0 :               child: Padding(
     613           0 :                 padding: EdgeInsets.all(10.0),
     614           0 :                 child: Padding(
     615           0 :                   padding: EdgeInsets.all(10.0),
     616           0 :                   child: Row(
     617             :                     mainAxisSize: MainAxisSize.min,
     618           0 :                     children: [
     619             :                       image,
     620           0 :                       Flexible(child: bubble),
     621             :                     ],
     622             :                   ),
     623             :                 ),
     624             :               ),
     625             :             ),
     626             :           );
     627             :         },
     628             :       );
     629             :     },
     630             :     context: context,
     631             :   );
     632             : }
     633             : 
     634             : // temporary until we do real picture selection
     635           0 : String RandomProfileImage(String onion) {
     636           0 :   var choices = [
     637             :     "001-centaur",
     638             :     "002-kraken",
     639             :     "003-dinosaur",
     640             :     "004-tree-1",
     641             :     "005-hand",
     642             :     "006-echidna",
     643             :     "007-robot",
     644             :     "008-mushroom",
     645             :     "009-harpy",
     646             :     "010-phoenix",
     647             :     "011-dragon-1",
     648             :     "012-devil",
     649             :     "013-troll",
     650             :     "014-alien",
     651             :     "015-minotaur",
     652             :     "016-madre-monte",
     653             :     "017-satyr",
     654             :     "018-karakasakozou",
     655             :     "019-pirate",
     656             :     "020-werewolf",
     657             :     "021-scarecrow",
     658             :     "022-valkyrie",
     659             :     "023-curupira",
     660             :     "024-loch-ness-monster",
     661             :     "025-tree",
     662             :     "026-cerberus",
     663             :     "027-gryphon",
     664             :     "028-mermaid",
     665             :     "029-vampire",
     666             :     "030-goblin",
     667             :     "031-yeti",
     668             :     "032-leprechaun",
     669             :     "033-medusa",
     670             :     "034-chimera",
     671             :     "035-elf",
     672             :     "036-hydra",
     673             :     "037-cyclops",
     674             :     "038-pegasus",
     675             :     "039-narwhal",
     676             :     "040-woodcutter",
     677             :     "041-zombie",
     678             :     "042-dragon",
     679             :     "043-frankenstein",
     680             :     "044-witch",
     681             :     "045-fairy",
     682             :     "046-genie",
     683             :     "047-pinocchio",
     684             :     "048-ghost",
     685             :     "049-wizard",
     686             :     "050-unicorn",
     687             :   ];
     688           0 :   var encoding = base32.decode(onion.toUpperCase());
     689           0 :   return "assets/profiles/" + choices[encoding[33] % choices.length] + ".png";
     690             : }

Generated by: LCOV version 1.14