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

Generated by: LCOV version 1.14