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

Generated by: LCOV version 1.14