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

Generated by: LCOV version 1.14