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

Generated by: LCOV version 1.14