LCOV - code coverage report
Current view: top level - lib/widgets - messagelist.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 110 0.0 %
Date: 2026-09-01 17:56:29 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:cwtch/models/appstate.dart';
       2             : import 'package:cwtch/models/contact.dart';
       3             : import 'package:cwtch/models/message.dart';
       4             : import 'package:cwtch/models/messagecache.dart';
       5             : import 'package:cwtch/models/profile.dart';
       6             : import 'package:cwtch/widgets/membersdrawer.dart';
       7             : import 'package:cwtch/widgets/messageloadingbubble.dart';
       8             : import 'package:flutter/material.dart';
       9             : import 'package:provider/provider.dart';
      10             : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
      11             : import 'package:cwtch/l10n/app_localizations.dart';
      12             : import '../main.dart';
      13             : import '../settings.dart';
      14             : 
      15             : class MessageList extends StatefulWidget {
      16             :   ItemPositionsListener scrollListener;
      17           0 :   MessageList(this.scrollListener);
      18             : 
      19           0 :   @override
      20           0 :   _MessageListState createState() => _MessageListState();
      21             : }
      22             : 
      23             : class _MessageListState extends State<MessageList> {
      24           0 :   @override
      25             :   Widget build(BuildContext outerContext) {
      26             :     // On Android we can have unsynced messages at the front of the index from when the UI was asleep, if there are some, kick off sync of those first
      27           0 :     if (Provider.of<ContactInfoState>(context).messageCache.indexUnsynced != 0) {
      28           0 :       var conversationId = Provider.of<AppState>(outerContext, listen: false).selectedConversation!;
      29           0 :       MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
      30           0 :       ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
      31             :     }
      32           0 :     if (Provider.of<ContactInfoState>(outerContext, listen: false).everFetched == false) {
      33           0 :       messageHandler(
      34             :         outerContext,
      35           0 :         Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
      36           0 :         Provider.of<ContactInfoState>(outerContext, listen: false).identifier,
      37           0 :         ByIndex(Provider.of<ContactInfoState>(outerContext, listen: false).uiLoadedMessages),
      38             :       );
      39           0 :       Provider.of<ContactInfoState>(outerContext, listen: false).everFetched = true;
      40             :     }
      41           0 :     bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
      42           0 :     bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
      43             : 
      44           0 :     bool preserveHistoryByDefault = Provider.of<Settings>(context, listen: false).preserveHistoryByDefault;
      45           0 :     bool showEphemeralWarning = (isP2P && (!preserveHistoryByDefault && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory"));
      46           0 :     bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
      47             :     bool showSyncing = isGroupAndSyncing;
      48           0 :     bool showHybridDisabled = Provider.of<ContactInfoState>(context).isManaged && !Provider.of<Settings>(context).isExperimentEnabled(GroupManagerExperiment);
      49             :     bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing || showHybridDisabled;
      50             : 
      51           0 :     return RepaintBoundary(
      52           0 :       child: Container(
      53           0 :         color: Provider.of<Settings>(context).theme.backgroundMainColor,
      54           0 :         child: Column(
      55           0 :           children: [
      56           0 :             Visibility(
      57             :               visible: showMessageWarning,
      58           0 :               child: Container(
      59           0 :                 padding: EdgeInsets.all(5.0),
      60           0 :                 color: Provider.of<Settings>(context).theme.backgroundHilightElementColor,
      61           0 :                 child: DefaultTextStyle(
      62           0 :                   style: TextStyle(color: Provider.of<Settings>(context).theme.hilightElementColor),
      63           0 :                   child: _chooseWarningMessage(showSyncing, showOfflineWarning, showEphemeralWarning, showHybridDisabled),
      64             :                 ),
      65             :               ),
      66             :             ),
      67           0 :             Expanded(
      68           0 :               child: Container(
      69             :                 // Only show broken heart is the contact is offline...
      70           0 :                 decoration: BoxDecoration(
      71           0 :                   image: Provider.of<ContactInfoState>(outerContext).isOnline()
      72           0 :                       ? (Provider.of<Settings>(context).themeImages && Provider.of<Settings>(context).theme.chatImage != null)
      73           0 :                             ? DecorationImage(
      74             :                                 repeat: ImageRepeat.repeat,
      75           0 :                                 image: Provider.of<Settings>(context).theme.loadImage(Provider.of<Settings>(context).theme.chatImage, context: context),
      76           0 :                                 colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.chatImageColor, BlendMode.srcIn),
      77             :                               )
      78             :                             : null
      79           0 :                       : DecorationImage(
      80             :                           fit: BoxFit.scaleDown,
      81             :                           alignment: Alignment.center,
      82           0 :                           image: AssetImage("assets/core/negative_heart_512px.png"),
      83           0 :                           colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn),
      84             :                         ),
      85             :                 ),
      86             :                 // Don't load messages for syncing server...
      87           0 :                 child: Padding(padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 20.0), child: _buildPaneStructure(outerContext)),
      88             :               ),
      89             :             ),
      90             :           ],
      91             :         ),
      92             :       ),
      93             :     );
      94             :   }
      95             : 
      96             :   // Only show one warning. This method encapsulates the logic of which warning to prioritize.
      97           0 :   Widget _chooseWarningMessage(showSyncing, showOfflineWarning, showEphemeralWarning, showHybridDisabled) {
      98             :     if (showHybridDisabled) {
      99           0 :       return Text(AppLocalizations.of(context)!.hybridNotEnabledWarning, textAlign: TextAlign.center);
     100             :     }
     101             : 
     102             :     if (showSyncing) {
     103           0 :       return Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center);
     104             :     }
     105             : 
     106             :     if (showOfflineWarning) {
     107             :       // if it's been more than 2 minutes since we last clicked the button let users click the button again
     108             :       // OR if users have never clicked the button AND they appear offline, then they can click the button
     109             :       // NOTE: all these listeners are false...this is not ideal, but if they were true we would end up rebuilding the message view every tick (which would kill performance)
     110             :       // any significant changes in state e.g. peer offline or button clicks will trigger a rebuild anyway
     111             :       bool canReconnect =
     112           0 :           DateTime.now().difference(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime).abs() > Duration(seconds: 30) ||
     113           0 :           (Provider.of<ProfileInfoState>(context, listen: false).appearOffline &&
     114           0 :               (Provider.of<ContactInfoState>(context, listen: false).lastRetryTime == Provider.of<ContactInfoState>(context, listen: false).loaded));
     115           0 :       var reconnectButton = Padding(
     116           0 :         padding: EdgeInsets.all(2),
     117             :         child: canReconnect
     118           0 :             ? Tooltip(
     119           0 :                 message: AppLocalizations.of(context)!.retryConnectionTooltip,
     120           0 :                 child: ElevatedButton(
     121           0 :                   style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
     122           0 :                   child: Text(AppLocalizations.of(context)!.retryConnection),
     123           0 :                   onPressed: () {
     124           0 :                     if (Provider.of<ContactInfoState>(context, listen: false).isGroup) {
     125           0 :                       Provider.of<FlwtchState>(
     126           0 :                         context,
     127             :                         listen: false,
     128           0 :                       ).cwtch.AttemptReconnectionServer(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).server!);
     129             :                     } else {
     130           0 :                       Provider.of<FlwtchState>(
     131           0 :                         context,
     132             :                         listen: false,
     133           0 :                       ).cwtch.AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
     134             :                     }
     135           0 :                     Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
     136           0 :                     Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
     137           0 :                     setState(() {
     138             :                       // force update of this view...otherwise the button won't be removed fast enough...
     139             :                     });
     140             :                   },
     141             :                 ),
     142             :               )
     143           0 :             : CircularProgressIndicator(color: Provider.of<Settings>(context).theme.hilightElementColor),
     144             :       );
     145           0 :       return Column(
     146             :         crossAxisAlignment: CrossAxisAlignment.center,
     147           0 :         children: [
     148           0 :           Text(
     149           0 :             Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
     150             :             textAlign: TextAlign.center,
     151             :           ),
     152             :           reconnectButton,
     153             :         ],
     154             :       );
     155             :     }
     156             : 
     157             :     if (showEphemeralWarning) {
     158             :       // Only show the ephemeral status for peer conversations, not for groups...
     159           0 :       return Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center);
     160             :     }
     161             : 
     162             :     // We are not allowed to put null here, so put an empty text widget
     163           0 :     return Text('');
     164             :   }
     165             : 
     166           0 :   Widget _buildPaneStructure(BuildContext outerContext) {
     167           0 :     if (Provider.of<ContactInfoState>(context).membersDrawerOpen) {
     168           0 :       return Flex(
     169             :         direction: Axis.horizontal,
     170           0 :         children: <Widget>[
     171           0 :           Flexible(flex: 4, child: _theList(outerContext)),
     172           0 :           Flexible(flex: 1, child: MembersDrawer(outerContext)),
     173             :         ],
     174             :       );
     175             :     } else {
     176           0 :       return _theList(outerContext);
     177             :     }
     178             :   }
     179             : 
     180           0 :   Widget _theList(BuildContext outerContext) {
     181           0 :     var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
     182           0 :     return ScrollablePositionedList.builder(
     183           0 :       itemPositionsListener: widget.scrollListener,
     184           0 :       itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
     185           0 :       initialScrollIndex: initi > 4 ? initi - 4 : 0,
     186           0 :       itemCount: Provider.of<ContactInfoState>(outerContext).uiLoadedMessages + 1,
     187             :       reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
     188             :       shrinkWrap: true,
     189           0 :       itemBuilder: (itemBuilderContext, index) {
     190           0 :         var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
     191           0 :         var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
     192             :         var messageIndex = index;
     193           0 :         var loadedMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).uiLoadedMessages;
     194           0 :         var endOfMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).totalMessages + 1;
     195           0 :         if (index == endOfMessages - 1) {
     196           0 :           return ListTile(title: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryEndOfHistory, textAlign: TextAlign.center), dense: true);
     197           0 :         } else if (index >= loadedMessages) {
     198           0 :           return ElevatedButton(
     199           0 :             child: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryLoadOlderMessages),
     200           0 :             key: Key("loadAdditionalMessages"),
     201           0 :             onPressed: () {
     202           0 :               messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex));
     203             :             },
     204             :           );
     205             :         }
     206           0 :         return FutureBuilder(
     207           0 :           future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
     208           0 :           builder: (fbcontext, snapshot) {
     209           0 :             if (snapshot.hasData) {
     210           0 :               var message = snapshot.data as Message;
     211             :               // here we create an index key for the contact and assign it to the row. Indexes are unique so we can
     212             :               // reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
     213             :               // when new messages are added...however it is better than the alternative of not having widget keys at all.
     214           0 :               var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
     215           0 :               return message.getWidget(fbcontext, key, messageIndex);
     216             :             } else {
     217           0 :               return MessageLoadingBubble();
     218             :             }
     219             :           },
     220             :         );
     221             :       },
     222             :     );
     223             :   }
     224             : }

Generated by: LCOV version 1.14