LCOV - code coverage report
Current view: top level - lib/widgets - messagelist.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 106 0.0 %
Date: 2026-08-30 22:38:45 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             :     bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing;
      49             : 
      50             :     // if it's been more than 2 minutes since we last clicked the button let users click the button again
      51             :     // OR if users have never clicked the button AND they appear offline, then they can click the button
      52             :     // 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)
      53             :     // any significant changes in state e.g. peer offline or button clicks will trigger a rebuild anyway
      54             :     bool canReconnect =
      55           0 :         DateTime.now().difference(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime).abs() > Duration(seconds: 30) ||
      56           0 :         (Provider.of<ProfileInfoState>(context, listen: false).appearOffline &&
      57           0 :             (Provider.of<ContactInfoState>(context, listen: false).lastRetryTime == Provider.of<ContactInfoState>(context, listen: false).loaded));
      58             : 
      59           0 :     var reconnectButton = Padding(
      60           0 :       padding: EdgeInsets.all(2),
      61             :       child: canReconnect
      62           0 :           ? Tooltip(
      63           0 :               message: AppLocalizations.of(context)!.retryConnectionTooltip,
      64           0 :               child: ElevatedButton(
      65           0 :                 style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
      66           0 :                 child: Text(AppLocalizations.of(context)!.retryConnection),
      67           0 :                 onPressed: () {
      68           0 :                   if (Provider.of<ContactInfoState>(context, listen: false).isGroup) {
      69           0 :                     Provider.of<FlwtchState>(
      70           0 :                       context,
      71             :                       listen: false,
      72           0 :                     ).cwtch.AttemptReconnectionServer(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).server!);
      73             :                   } else {
      74           0 :                     Provider.of<FlwtchState>(
      75           0 :                       context,
      76             :                       listen: false,
      77           0 :                     ).cwtch.AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
      78             :                   }
      79           0 :                   Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
      80           0 :                   Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
      81           0 :                   setState(() {
      82             :                     // force update of this view...otherwise the button won't be removed fast enough...
      83             :                   });
      84             :                 },
      85             :               ),
      86             :             )
      87           0 :           : CircularProgressIndicator(color: Provider.of<Settings>(context).theme.hilightElementColor),
      88             :     );
      89             : 
      90           0 :     return RepaintBoundary(
      91           0 :       child: Container(
      92           0 :         color: Provider.of<Settings>(context).theme.backgroundMainColor,
      93           0 :         child: Column(
      94           0 :           children: [
      95           0 :             Visibility(
      96             :               visible: showMessageWarning,
      97           0 :               child: Container(
      98           0 :                 padding: EdgeInsets.all(5.0),
      99           0 :                 color: Provider.of<Settings>(context).theme.backgroundHilightElementColor,
     100           0 :                 child: DefaultTextStyle(
     101           0 :                   style: TextStyle(color: Provider.of<Settings>(context).theme.hilightElementColor),
     102             :                   child: showSyncing
     103           0 :                       ? Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center)
     104             :                       : showOfflineWarning
     105           0 :                       ? Column(
     106             :                           crossAxisAlignment: CrossAxisAlignment.center,
     107           0 :                           children: [
     108           0 :                             Text(
     109           0 :                               Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
     110             :                               textAlign: TextAlign.center,
     111             :                             ),
     112             :                             reconnectButton,
     113             :                           ],
     114             :                         )
     115             :                       // Only show the ephemeral status for peer conversations, not for groups...
     116             :                       : (showEphemeralWarning
     117           0 :                             ? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
     118             :                             :
     119             :                               // We are not allowed to put null here, so put an empty text widget
     120           0 :                               Text("")),
     121             :                 ),
     122             :               ),
     123             :             ),
     124           0 :             Expanded(
     125           0 :               child: Container(
     126             :                 // Only show broken heart is the contact is offline...
     127           0 :                 decoration: BoxDecoration(
     128           0 :                   image: Provider.of<ContactInfoState>(outerContext).isOnline()
     129           0 :                       ? (Provider.of<Settings>(context).themeImages && Provider.of<Settings>(context).theme.chatImage != null)
     130           0 :                             ? DecorationImage(
     131             :                                 repeat: ImageRepeat.repeat,
     132           0 :                                 image: Provider.of<Settings>(context).theme.loadImage(Provider.of<Settings>(context).theme.chatImage, context: context),
     133           0 :                                 colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.chatImageColor, BlendMode.srcIn),
     134             :                               )
     135             :                             : null
     136           0 :                       : DecorationImage(
     137             :                           fit: BoxFit.scaleDown,
     138             :                           alignment: Alignment.center,
     139           0 :                           image: AssetImage("assets/core/negative_heart_512px.png"),
     140           0 :                           colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn),
     141             :                         ),
     142             :                 ),
     143             :                 // Don't load messages for syncing server...
     144           0 :                 child: Padding(padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 20.0), child: _buildPaneStructure(outerContext)),
     145             :               ),
     146             :             ),
     147             :           ],
     148             :         ),
     149             :       ),
     150             :     );
     151             :   }
     152             : 
     153           0 :   Widget _buildPaneStructure(BuildContext outerContext) {
     154           0 :     if (Provider.of<ContactInfoState>(context).membersDrawerOpen) {
     155           0 :       return Flex(
     156             :         direction: Axis.horizontal,
     157           0 :         children: <Widget>[
     158           0 :           Flexible(flex: 4, child: _theList(outerContext)),
     159           0 :           Flexible(flex: 1, child: MembersDrawer(outerContext)),
     160             :         ],
     161             :       );
     162             :     } else {
     163           0 :       return _theList(outerContext);
     164             :     }
     165             :   }
     166             : 
     167           0 :   Widget _theList(BuildContext outerContext) {
     168           0 :     var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
     169           0 :     return ScrollablePositionedList.builder(
     170           0 :       itemPositionsListener: widget.scrollListener,
     171           0 :       itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
     172           0 :       initialScrollIndex: initi > 4 ? initi - 4 : 0,
     173           0 :       itemCount: Provider.of<ContactInfoState>(outerContext).uiLoadedMessages + 1,
     174             :       reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
     175             :       shrinkWrap: true,
     176           0 :       itemBuilder: (itemBuilderContext, index) {
     177           0 :         var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
     178           0 :         var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
     179             :         var messageIndex = index;
     180           0 :         var loadedMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).uiLoadedMessages;
     181           0 :         var endOfMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).totalMessages + 1;
     182           0 :         if (index == endOfMessages - 1) {
     183           0 :           return ListTile(title: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryEndOfHistory, textAlign: TextAlign.center), dense: true);
     184           0 :         } else if (index >= loadedMessages) {
     185           0 :           return ElevatedButton(
     186           0 :             child: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryLoadOlderMessages),
     187           0 :             key: Key("loadAdditionalMessages"),
     188           0 :             onPressed: () {
     189           0 :               messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex));
     190             :             },
     191             :           );
     192             :         }
     193           0 :         return FutureBuilder(
     194           0 :           future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
     195           0 :           builder: (fbcontext, snapshot) {
     196           0 :             if (snapshot.hasData) {
     197           0 :               var message = snapshot.data as Message;
     198             :               // here we create an index key for the contact and assign it to the row. Indexes are unique so we can
     199             :               // reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
     200             :               // when new messages are added...however it is better than the alternative of not having widget keys at all.
     201           0 :               var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
     202           0 :               return message.getWidget(fbcontext, key, messageIndex);
     203             :             } else {
     204           0 :               return MessageLoadingBubble();
     205             :             }
     206             :           },
     207             :         );
     208             :       },
     209             :     );
     210             :   }
     211             : }

Generated by: LCOV version 1.14