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

Generated by: LCOV version 1.14