LCOV - code coverage report
Current view: top level - lib/widgets - invitationbubble.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 103 0.0 %
Date: 2026-08-31 22:32:18 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:cwtch/cwtch_icons_icons.dart';
       2             : import 'package:cwtch/models/contact.dart';
       3             : import 'package:cwtch/models/message.dart';
       4             : import 'package:cwtch/models/profile.dart';
       5             : import 'package:cwtch/themes/opaque.dart';
       6             : import 'package:cwtch/widgets/malformedbubble.dart';
       7             : import 'package:flutter/material.dart';
       8             : import 'package:provider/provider.dart';
       9             : import 'package:cwtch/l10n/app_localizations.dart';
      10             : 
      11             : import '../constants.dart';
      12             : import '../main.dart';
      13             : import '../models/redaction.dart';
      14             : import '../settings.dart';
      15             : import 'messageBubbleWidgetHelpers.dart';
      16             : import 'messagebubbledecorations.dart';
      17             : 
      18             : // Like MessageBubble but for displaying chat overlay 100/101 invitations
      19             : // Offers the user an accept/reject button if they don't have a matching contact already
      20             : class InvitationBubble extends StatefulWidget {
      21             :   final int overlay;
      22             :   final String inviteTarget;
      23             :   final String inviteNick;
      24             :   final String invite;
      25             : 
      26           0 :   InvitationBubble(this.overlay, this.inviteTarget, this.inviteNick, this.invite);
      27             : 
      28           0 :   @override
      29           0 :   InvitationBubbleState createState() => InvitationBubbleState();
      30             : }
      31             : 
      32             : class InvitationBubbleState extends State<InvitationBubble> {
      33             :   bool rejected = false;
      34             :   bool isAccepted = false;
      35             : 
      36           0 :   @override
      37             :   Widget build(BuildContext context) {
      38           0 :     var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
      39           0 :     var isGroup = widget.overlay == InviteGroupOverlay || widget.invite.startsWith(ManagedGroupPrefix);
      40           0 :     var isHybridGroup = widget.invite.startsWith(ManagedGroupPrefix);
      41           0 :     isAccepted = Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget) != null;
      42             :     var borderRadiousEh = 15.0;
      43           0 :     var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
      44           0 :     var showHybirdGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(GroupManagerExperiment);
      45           0 :     rejected = Provider.of<MessageMetadata>(context).attributes["rejected-invite"] == "true";
      46           0 :     DateTime messageDate = Provider.of<MessageMetadata>(context).timestamp;
      47             : 
      48             :     // If the sender is not us, then we want to give them a nickname...
      49             :     var senderDisplayStr = "";
      50             :     if (!fromMe) {
      51           0 :       ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
      52             :       if (contact != null) {
      53           0 :         senderDisplayStr = redactedNick(context, contact.onion, contact.nickname);
      54             :       } else {
      55           0 :         senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
      56             :       }
      57             :     }
      58           0 :     return LayoutBuilder(
      59           0 :       builder: (context, constraints) {
      60           0 :         var wdgSender = compileSenderWidget(context, constraints, fromMe, senderDisplayStr);
      61             :         // If we receive an invite for ourselves, treat it as a bug. The UI no longer allows this so it could have only come from
      62             :         // some kind of malfeasance.
      63           0 :         var selfInvite = widget.inviteNick == Provider.of<ProfileInfoState>(context).onion;
      64             :         if (selfInvite) {
      65           0 :           return MalformedBubble();
      66             :         }
      67             : 
      68             :         var wdgMessage = isGroup && !showGroupInvite
      69           0 :             ? Text(AppLocalizations.of(context)!.groupInviteSettingsWarning, style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle))
      70             :             : isHybridGroup && !showHybirdGroupInvite
      71           0 :             ? Text(AppLocalizations.of(context)!.hybridGroupInviteSettingsWarning, style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle))
      72             :             : fromMe
      73           0 :             ? senderInviteChrome(
      74           0 :                 AppLocalizations.of(context)!.sendAnInvitation,
      75           0 :                 isGroup ? Provider.of<ProfileInfoState>(context).contactList.findContact(widget.inviteTarget)!.nickname : widget.inviteTarget,
      76             :               )
      77           0 :             : (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, widget.inviteNick, widget.inviteTarget));
      78             : 
      79             :         Widget wdgDecorations;
      80             :         if (isGroup && !showGroupInvite) {
      81           0 :           wdgDecorations = Text('\u202F');
      82             :         } else if (fromMe) {
      83           0 :           wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageMetadata>(context).ackd, errored: Provider.of<MessageMetadata>(context).error, fromMe: fromMe, messageDate: messageDate);
      84           0 :         } else if (isAccepted) {
      85           0 :           wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
      86           0 :         } else if (this.rejected) {
      87           0 :           wdgDecorations = Text(AppLocalizations.of(context)!.rejected + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextStyle));
      88             :         } else {
      89           0 :           wdgDecorations = Center(
      90             :             widthFactor: 1,
      91           0 :             child: Wrap(
      92           0 :               children: [
      93           0 :                 Padding(
      94           0 :                   padding: EdgeInsets.all(5),
      95           0 :                   child: ElevatedButton(
      96           0 :                     child: Text(AppLocalizations.of(context)!.rejectGroupBtn + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
      97           0 :                     onPressed: _btnReject,
      98             :                   ),
      99             :                 ),
     100           0 :                 Padding(
     101           0 :                   padding: EdgeInsets.all(5),
     102           0 :                   child: ElevatedButton(
     103           0 :                     child: Text(AppLocalizations.of(context)!.acceptGroupBtn + '\u202F', style: Provider.of<Settings>(context).scaleFonts(defaultTextButtonStyle)),
     104           0 :                     onPressed: _btnAccept,
     105             :                   ),
     106             :                 ),
     107             :               ],
     108             :             ),
     109             :           );
     110             :         }
     111             : 
     112             :         //print(constraints.toString()+", "+constraints.maxWidth.toString());
     113           0 :         return Center(
     114             :           widthFactor: 1.0,
     115           0 :           child: Theme(
     116           0 :             data: Theme.of(context).copyWith(
     117           0 :               textSelectionTheme: TextSelectionThemeData(
     118           0 :                 cursorColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     119           0 :                 selectionColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     120           0 :                 selectionHandleColor: Provider.of<Settings>(context).theme.messageSelectionColor,
     121             :               ),
     122             : 
     123             :               // Horrifying Hack: Flutter doesn't give us direct control over system menus but instead picks BG color from TextButtonThemeData ¯\_(ツ)_/¯
     124           0 :               textButtonTheme: TextButtonThemeData(style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Provider.of<Settings>(context).theme.menuBackgroundColor))),
     125             :             ),
     126           0 :             child: Container(
     127           0 :               decoration: BoxDecoration(
     128           0 :                 color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor,
     129           0 :                 border: Border.all(color: fromMe ? Provider.of<Settings>(context).theme.messageFromMeBackgroundColor : Provider.of<Settings>(context).theme.messageFromOtherBackgroundColor, width: 1),
     130           0 :                 borderRadius: BorderRadius.only(
     131           0 :                   topLeft: Radius.circular(borderRadiousEh),
     132           0 :                   topRight: Radius.circular(borderRadiousEh),
     133           0 :                   bottomLeft: fromMe ? Radius.circular(borderRadiousEh) : Radius.zero,
     134           0 :                   bottomRight: fromMe ? Radius.zero : Radius.circular(borderRadiousEh),
     135             :                 ),
     136             :               ),
     137           0 :               child: Center(
     138             :                 widthFactor: 1.0,
     139           0 :                 child: Padding(
     140           0 :                   padding: EdgeInsets.all(9.0),
     141           0 :                   child: Wrap(
     142             :                     runAlignment: WrapAlignment.spaceEvenly,
     143             :                     alignment: WrapAlignment.spaceEvenly,
     144             :                     runSpacing: 1.0,
     145             :                     crossAxisAlignment: WrapCrossAlignment.center,
     146           0 :                     children: [
     147           0 :                       Center(
     148             :                         widthFactor: 1,
     149           0 :                         child: Padding(padding: EdgeInsets.all(10.0), child: Icon(isGroup && !showGroupInvite ? CwtchIcons.enable_experiments : CwtchIcons.send_invitation, size: 32)),
     150             :                       ),
     151           0 :                       Center(
     152             :                         widthFactor: 1.0,
     153           0 :                         child: Column(
     154             :                           crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
     155             :                           mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start,
     156             :                           mainAxisSize: MainAxisSize.min,
     157           0 :                           children: fromMe ? [wdgMessage, wdgDecorations] : [wdgSender, wdgMessage, wdgDecorations],
     158             :                         ),
     159             :                       ),
     160             :                     ],
     161             :                   ),
     162             :                 ),
     163             :               ),
     164             :             ),
     165             :           ),
     166             :         );
     167             :       },
     168             :     );
     169             :   }
     170             : 
     171           0 :   void _btnReject() {
     172           0 :     setState(() {
     173           0 :       var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
     174           0 :       var conversation = Provider.of<ContactInfoState>(context, listen: false).identifier;
     175           0 :       var idx = Provider.of<MessageMetadata>(context, listen: false).messageID;
     176           0 :       Provider.of<FlwtchState>(context, listen: false).cwtch.SetMessageAttribute(profileOnion, conversation, 0, idx, "rejected-invite", "true");
     177             :       //Provider.of<MessageMetadata>(context, listen: false).flags |= 0x01;
     178             :     });
     179             :   }
     180             : 
     181           0 :   void _btnAccept() {
     182           0 :     setState(() {
     183           0 :       var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
     184           0 :       Provider.of<FlwtchState>(context, listen: false).cwtch.ImportBundle(profileOnion, widget.invite);
     185           0 :       isAccepted = true;
     186             :     });
     187             :   }
     188             : 
     189             :   // Construct an invite chrome for the sender
     190           0 :   Widget senderInviteChrome(String chrome, String targetName) {
     191           0 :     var settings = Provider.of<Settings>(context);
     192             : 
     193           0 :     return Wrap(
     194           0 :       children: [
     195           0 :         SelectableText(
     196           0 :           chrome + '\u202F',
     197           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     198             :           textAlign: TextAlign.left,
     199             :           maxLines: 2,
     200             :           textWidthBasis: TextWidthBasis.longestLine,
     201             :         ),
     202           0 :         SelectableText(
     203           0 :           targetName + '\u202F',
     204           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromMeTextColor)),
     205             :           textAlign: TextAlign.left,
     206             :           maxLines: 2,
     207             :           textWidthBasis: TextWidthBasis.longestLine,
     208             :         ),
     209             :       ],
     210             :     );
     211             :   }
     212             : 
     213             :   // Construct an invite chrome
     214           0 :   Widget inviteChrome(String chrome, String targetName, String targetId) {
     215           0 :     var settings = Provider.of<Settings>(context);
     216             : 
     217           0 :     return Wrap(
     218           0 :       children: [
     219           0 :         SelectableText(
     220           0 :           chrome + '\u202F',
     221           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     222             :           textAlign: TextAlign.left,
     223             :           textWidthBasis: TextWidthBasis.longestLine,
     224             :           maxLines: 2,
     225             :         ),
     226           0 :         SelectableText(
     227           0 :           targetName + '\u202F',
     228           0 :           style: settings.scaleFonts(defaultMessageTextStyle.copyWith(color: Provider.of<Settings>(context).theme.messageFromOtherTextColor)),
     229             :           textAlign: TextAlign.left,
     230             :           maxLines: 2,
     231             :           textWidthBasis: TextWidthBasis.longestLine,
     232             :         ),
     233             :       ],
     234             :     );
     235             :   }
     236             : }

Generated by: LCOV version 1.14