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

          Line data    Source code
       1             : import 'dart:convert';
       2             : import 'dart:math';
       3             : 
       4             : import 'package:cwtch/config.dart';
       5             : import 'package:cwtch/models/groupmembers.dart';
       6             : import 'package:cwtch/models/remoteserver.dart';
       7             : import 'package:cwtch/models/search.dart';
       8             : import 'package:flutter/widgets.dart';
       9             : import 'package:provider/provider.dart';
      10             : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
      11             : 
      12             : import '../main.dart';
      13             : import '../themes/opaque.dart';
      14             : import '../views/contactsview.dart';
      15             : import 'contact.dart';
      16             : import 'contactlist.dart';
      17             : import 'filedownloadprogress.dart';
      18             : import 'profileservers.dart';
      19             : import 'search.dart';
      20             : 
      21             : class ProfileInfoState extends ChangeNotifier {
      22             :   ProfileServerListState _servers = ProfileServerListState();
      23             :   ContactListState _contacts = ContactListState();
      24             :   SearchState _searchState = SearchState();
      25             :   final String onion;
      26             :   String _nickname = "";
      27             :   String _privateName = "";
      28             :   String _imagePath = "";
      29             :   String _defaultImagePath = "";
      30             :   int _unreadMessages = 0;
      31             :   bool _online = false;
      32             :   Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
      33             :   Map<String, int> _downloadTriggers = Map<String, int>();
      34             :   ItemScrollController contactListScrollController = new ItemScrollController();
      35             :   // assume profiles are encrypted...this will be set to false
      36             :   // in the constructor if the profile is encrypted with the defacto password.
      37             :   bool _encrypted = true;
      38             : 
      39             :   bool _autostart = true;
      40             :   bool _enabled = false;
      41             :   bool _appearOffline = false;
      42             :   bool _appearOfflineAtStartup = false;
      43             : 
      44           0 :   ProfileInfoState({
      45             :     required this.onion,
      46             :     nickname = "",
      47             :     privateName = "",
      48             :     imagePath = "",
      49             :     defaultImagePath = "",
      50             :     unreadMessages = 0,
      51             :     contactsJson = "",
      52             :     serversJson = "",
      53             :     online = false,
      54             :     autostart = true,
      55             :     encrypted = true,
      56             :     appearOffline = false,
      57             :   }) {
      58           0 :     this._nickname = nickname;
      59           0 :     this._privateName = privateName;
      60           0 :     this._imagePath = imagePath;
      61           0 :     this._defaultImagePath = defaultImagePath;
      62           0 :     this._unreadMessages = unreadMessages;
      63           0 :     this._online = online;
      64           0 :     this._enabled = _enabled;
      65           0 :     this._autostart = autostart;
      66             :     if (autostart) {
      67           0 :       this._enabled = true;
      68             :     }
      69           0 :     this._appearOffline = appearOffline;
      70           0 :     this._appearOfflineAtStartup = appearOffline;
      71           0 :     this._encrypted = encrypted;
      72             : 
      73           0 :     _contacts.connectServers(this._servers);
      74             : 
      75           0 :     if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
      76           0 :       this.replaceServers(serversJson);
      77             : 
      78           0 :       List<dynamic> contacts = jsonDecode(contactsJson);
      79           0 :       this._contacts.addAll(
      80           0 :         contacts.map((contact) {
      81           0 :           this._unreadMessages += contact["numUnread"] as int;
      82           0 :           ContactInfoState cis = ContactInfoState(
      83           0 :             this.onion,
      84           0 :             contact["identifier"],
      85           0 :             contact["onion"],
      86           0 :             nickname: contact["name"],
      87           0 :             localNickname: contact["attributes"]?["local.profile.name"] ?? "", // contact may not have a local name
      88           0 :             status: contact["status"],
      89           0 :             imagePath: contact["picture"],
      90           0 :             defaultImagePath: contact["isGroup"] ? contact["picture"] : contact["defaultPicture"],
      91           0 :             accepted: contact["accepted"],
      92           0 :             blocked: contact["blocked"],
      93           0 :             savePeerHistory: contact["saveConversationHistory"],
      94           0 :             numMessages: contact["numMessages"],
      95           0 :             numUnread: contact["numUnread"],
      96           0 :             isGroup: contact["isGroup"],
      97           0 :             server: contact["groupServer"],
      98           0 :             archived: contact["isArchived"] == true,
      99           0 :             lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
     100           0 :             pinned: contact["attributes"]?["local.profile.pinned"] == "true",
     101           0 :             notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default",
     102           0 :             isManaged: contact["managed"],
     103             :           );
     104           0 :           if (contact["managed"]) {
     105           0 :             cis.modeLine = contact["mode"] ?? "";
     106           0 :             cis.modeMask = contact["modeMask"] ?? "";
     107           0 :             cis.members = contact["accessControlList"].map<String, GroupMember>((k, v) => MapEntry<String, GroupMember>(k, GroupMember(k, v.cast<String, dynamic>()))).values.toList();
     108             :           }
     109           0 :           if (contact.containsKey("attributes")) {
     110           0 :             if (contact["attributes"].containsKey("local.conversation.shadowed") && contact["attributes"]["local.conversation.shadowed"] == "true") {
     111           0 :               cis.isShadowed = true;
     112             :             }
     113             :           }
     114             :           return cis;
     115             :         }),
     116             :       );
     117             : 
     118             :       // dummy set to invoke sort-on-load
     119           0 :       if (this._contacts.num > 0) {
     120           0 :         this._contacts.updateLastMessageReceivedTime(this._contacts.contacts.first.identifier, this._contacts.contacts.first.lastMessageReceivedTime);
     121             :       }
     122             :     }
     123             :   }
     124             : 
     125             :   // Parse out the server list json into our server info state struct...
     126           0 :   void replaceServers(String serversJson) {
     127           0 :     if (serversJson != "" && serversJson != "null") {
     128           0 :       List<dynamic> servers = jsonDecode(serversJson);
     129           0 :       this._servers.replace(
     130           0 :         servers.map((server) {
     131             :           // TODO Keys...
     132           0 :           var preSyncStartTime = DateTime.tryParse(server["syncProgress"]["startTime"]);
     133           0 :           var lastMessageTime = DateTime.tryParse(server["syncProgress"]["lastMessageTime"]);
     134           0 :           return RemoteServerInfoState(
     135           0 :             server["onion"],
     136           0 :             server["identifier"],
     137           0 :             server["description"],
     138           0 :             server["status"],
     139             :             lastPreSyncMessageTime: preSyncStartTime,
     140             :             mostRecentMessageTime: lastMessageTime,
     141             :           );
     142             :         }),
     143             :       );
     144             : 
     145           0 :       this._contacts.contacts.forEach((contact) {
     146           0 :         if (contact.isGroup) {
     147           0 :           _servers.addGroup(contact);
     148             :         }
     149             :       });
     150             : 
     151           0 :       notifyListeners();
     152             :     }
     153             :   }
     154             : 
     155             :   //
     156           0 :   void updateServerStatusCache(String server, String status) {
     157           0 :     this._servers.updateServerState(server, status);
     158           0 :     notifyListeners();
     159             :   }
     160             : 
     161             :   //  Getters and Setters for Online Status
     162           0 :   bool get isOnline => this._online;
     163             : 
     164           0 :   set isOnline(bool newValue) {
     165           0 :     this._online = newValue;
     166           0 :     notifyListeners();
     167             :   }
     168             : 
     169             :   // Check encrypted status for profile info screen
     170           0 :   bool get isEncrypted => this._encrypted;
     171           0 :   set isEncrypted(bool newValue) {
     172           0 :     this._encrypted = newValue;
     173           0 :     notifyListeners();
     174             :   }
     175             : 
     176           0 :   String get nickname => this._nickname;
     177             : 
     178           0 :   set nickname(String newValue) {
     179           0 :     this._nickname = newValue;
     180           0 :     notifyListeners();
     181             :   }
     182             : 
     183           0 :   String get imagePath => this._imagePath;
     184             : 
     185           0 :   set imagePath(String newVal) {
     186           0 :     this._imagePath = newVal;
     187           0 :     notifyListeners();
     188             :   }
     189             : 
     190           0 :   bool get enabled => this._enabled;
     191             : 
     192           0 :   set enabled(bool newVal) {
     193           0 :     this._enabled = newVal;
     194           0 :     notifyListeners();
     195             :   }
     196             : 
     197           0 :   bool get autostart => this._autostart;
     198           0 :   set autostart(bool newVal) {
     199           0 :     this._autostart = newVal;
     200           0 :     notifyListeners();
     201             :   }
     202             : 
     203           0 :   bool get appearOfflineAtStartup => this._appearOfflineAtStartup;
     204           0 :   set appearOfflineAtStartup(bool newVal) {
     205           0 :     this._appearOfflineAtStartup = newVal;
     206           0 :     notifyListeners();
     207             :   }
     208             : 
     209           0 :   bool get appearOffline => this._appearOffline;
     210           0 :   set appearOffline(bool newVal) {
     211           0 :     this._appearOffline = newVal;
     212           0 :     notifyListeners();
     213             :   }
     214             : 
     215           0 :   String get defaultImagePath => this._defaultImagePath;
     216             : 
     217           0 :   set defaultImagePath(String newVal) {
     218           0 :     this._defaultImagePath = newVal;
     219           0 :     notifyListeners();
     220             :   }
     221             : 
     222           0 :   int get unreadMessages => this._unreadMessages;
     223             : 
     224           0 :   set unreadMessages(int newVal) {
     225           0 :     this._unreadMessages = newVal;
     226           0 :     notifyListeners();
     227             :   }
     228             : 
     229           0 :   void recountUnread() {
     230           0 :     this._unreadMessages = _contacts.contacts.fold(0, (i, c) => i + c.unreadMessages);
     231             :   }
     232             : 
     233             :   // Remove a contact from a list. Currently only used when rejecting a group invitation.
     234             :   // Eventually will also be used for other removals.
     235           0 :   void removeContact(String handle) {
     236           0 :     this.contactList.removeContactByHandle(handle);
     237           0 :     notifyListeners();
     238             :   }
     239             : 
     240           0 :   ContactListState get contactList => this._contacts;
     241             : 
     242           0 :   ProfileServerListState get serverList => this._servers;
     243             : 
     244           0 :   SearchState get searchState => this._searchState;
     245             : 
     246           0 :   List<ContactInfoState> filteredList() {
     247           0 :     var clist = this._contacts.contacts.where((ContactInfoState c) => !c.isShadowed);
     248           0 :     if (!this._searchState.isFiltered) return clist.toList();
     249           0 :     return clist.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(this._searchState.filter) || (c.nickname.toLowerCase().contains(this._searchState.filter))).toList();
     250             :   }
     251             : 
     252           0 :   @override
     253             :   void dispose() {
     254           0 :     super.dispose();
     255             :   }
     256             : 
     257           0 :   void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
     258           0 :     this._nickname = name;
     259           0 :     this._imagePath = picture;
     260           0 :     this._online = online;
     261           0 :     this._unreadMessages = 0;
     262           0 :     this.replaceServers(serverJson);
     263             : 
     264           0 :     if (contactsJson != "" && contactsJson != "null") {
     265           0 :       List<dynamic> contacts = jsonDecode(contactsJson);
     266           0 :       contacts.forEach((contact) {
     267           0 :         var profileContact = this._contacts.getContact(contact["identifier"]);
     268           0 :         this._unreadMessages += contact["numUnread"] as int;
     269             :         if (profileContact != null) {
     270           0 :           profileContact.status = contact["status"];
     271             : 
     272           0 :           var newCount = contact["numMessages"] as int;
     273           0 :           if (newCount != profileContact.totalMessages) {
     274           0 :             if (newCount < profileContact.totalMessages) {
     275             :               // on Android, when sharing a file the UI may be briefly unloaded for the
     276             :               // OS to display the file management/selection screen. Afterwards a
     277             :               // call to ReconnectCwtchForeground will be made which will refresh all values (including count of numMessages)
     278             :               // **at the same time** the foreground will increment .totalMessages and send a new message to the backend.
     279             :               // This will result in a negative number of messages being calculated here, and an incorrect totalMessage count.
     280             :               // This bug is exacerbated in debug mode, and when multiple files are sent in succession. Both cases result in multiple ReconnectCwtchForeground
     281             :               // events that have the potential to conflict with currentMessageCounts.
     282             :               // Note that *if* a new message came in at the same time, we would be unable to distinguish this case - as such this is specific instance of a more general problem
     283             :               // TODO: A true-fix to this bug is to implement a syncing step in the foreground where totalMessages and inFlightMessages can be distinguished
     284             :               // This requires a change to the backend to confirm submission of an inFlightMessage, which will be implemented in #664
     285           0 :               EnvironmentConfig.debugLog("Conflicting message counts: $newCount ${profileContact.totalMessages}");
     286           0 :               newCount = max(newCount, profileContact.totalMessages);
     287             :             }
     288           0 :             profileContact.messageCache.addFrontIndexGap(newCount - profileContact.totalMessages);
     289             :           }
     290           0 :           profileContact.totalMessages = newCount;
     291           0 :           profileContact.unreadMessages = contact["numUnread"];
     292           0 :           profileContact.lastMessageReceivedTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]));
     293             :         } else {
     294           0 :           this._contacts.add(
     295           0 :             ContactInfoState(
     296           0 :               this.onion,
     297           0 :               contact["identifier"],
     298           0 :               contact["onion"],
     299           0 :               nickname: contact["name"],
     300           0 :               defaultImagePath: contact["defaultPicture"],
     301           0 :               status: contact["status"],
     302           0 :               imagePath: contact["picture"],
     303           0 :               accepted: contact["accepted"],
     304           0 :               blocked: contact["blocked"],
     305           0 :               savePeerHistory: contact["saveConversationHistory"],
     306           0 :               numMessages: contact["numMessages"],
     307           0 :               numUnread: contact["numUnread"],
     308           0 :               isGroup: contact["isGroup"],
     309           0 :               server: contact["groupServer"],
     310           0 :               lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])),
     311           0 :               notificationPolicy: contact["notificationPolicy"] ?? "ConversationNotificationPolicy.Default",
     312           0 :               isManaged: contact["managed"] == "true",
     313             :             ),
     314             :           );
     315             :         }
     316             :       });
     317             :     }
     318           0 :     resortContacts();
     319             :   }
     320             : 
     321           0 :   void newMessage(
     322             :     int identifier,
     323             :     int messageID,
     324             :     DateTime timestamp,
     325             :     String senderHandle,
     326             :     String senderImage,
     327             :     bool isAuto,
     328             :     String data,
     329             :     String contenthash,
     330             :     bool selectedProfile,
     331             :     bool selectedConversation,
     332             :     String signature,
     333             :   ) {
     334             :     if (!selectedProfile) {
     335           0 :       unreadMessages++;
     336             :     }
     337             : 
     338           0 :     contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation, signature);
     339           0 :     notifyListeners();
     340             :   }
     341             : 
     342           0 :   void resortContacts() {
     343           0 :     _contacts.resort();
     344           0 :     notifyListeners();
     345             :   }
     346             : 
     347           0 :   void downloadInit(String fileKey, int numChunks) {
     348           0 :     this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
     349           0 :     notifyListeners();
     350             :   }
     351             : 
     352           0 :   void downloadUpdate(String fileKey, int progress, int numChunks) {
     353           0 :     if (!downloadActive(fileKey)) {
     354           0 :       this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now());
     355           0 :       if (progress < 0) {
     356           0 :         this._downloads[fileKey]!.interrupted = true;
     357             :       }
     358             :     } else {
     359           0 :       if (this._downloads[fileKey]!.interrupted) {
     360           0 :         this._downloads[fileKey]!.interrupted = false;
     361             :       }
     362           0 :       this._downloads[fileKey]!.chunksDownloaded = progress;
     363           0 :       this._downloads[fileKey]!.chunksTotal = numChunks;
     364           0 :       this._downloads[fileKey]!.markUpdate();
     365             :     }
     366           0 :     notifyListeners();
     367             :   }
     368             : 
     369           0 :   void downloadMarkManifest(String fileKey) {
     370           0 :     if (!downloadActive(fileKey)) {
     371           0 :       this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now());
     372             :     }
     373           0 :     this._downloads[fileKey]!.gotManifest = true;
     374           0 :     this._downloads[fileKey]!.markUpdate();
     375           0 :     notifyListeners();
     376             :   }
     377             : 
     378           0 :   void downloadMarkFinished(String fileKey, String finalPath) {
     379           0 :     if (!downloadActive(fileKey)) {
     380             :       // happens as a result of a CheckDownloadStatus call,
     381             :       // invoked from a historical (timeline) download message
     382             :       // so setting numChunks correctly shouldn't matter
     383           0 :       this.downloadInit(fileKey, 1);
     384             :     }
     385             : 
     386             :     // Update the contact with a custom profile image if we are
     387             :     // waiting for one...
     388           0 :     if (this._downloadTriggers.containsKey(fileKey)) {
     389           0 :       int identifier = this._downloadTriggers[fileKey]!;
     390           0 :       this.contactList.getContact(identifier)!.imagePath = finalPath;
     391           0 :       notifyListeners();
     392             :     }
     393             : 
     394             :     // only update if different
     395           0 :     if (!this._downloads[fileKey]!.complete) {
     396           0 :       this._downloads[fileKey]!.timeEnd = DateTime.now();
     397           0 :       this._downloads[fileKey]!.downloadedTo = finalPath;
     398           0 :       this._downloads[fileKey]!.complete = true;
     399           0 :       this._downloads[fileKey]!.markUpdate();
     400           0 :       notifyListeners();
     401             :     }
     402             :   }
     403             : 
     404           0 :   bool downloadKnown(String fileKey) {
     405           0 :     return this._downloads.containsKey(fileKey);
     406             :   }
     407             : 
     408           0 :   bool downloadActive(String fileKey) {
     409           0 :     return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted;
     410             :   }
     411             : 
     412           0 :   bool downloadGotManifest(String fileKey) {
     413           0 :     return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest;
     414             :   }
     415             : 
     416           0 :   bool downloadComplete(String fileKey) {
     417           0 :     return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete;
     418             :   }
     419             : 
     420           0 :   bool downloadInterrupted(String fileKey) {
     421           0 :     if (this._downloads.containsKey(fileKey)) {
     422           0 :       if (this._downloads[fileKey]!.interrupted) {
     423             :         return true;
     424             :       }
     425             :     }
     426             :     return false;
     427             :   }
     428             : 
     429           0 :   void downloadMarkResumed(String fileKey) {
     430           0 :     if (this._downloads.containsKey(fileKey)) {
     431           0 :       this._downloads[fileKey]!.interrupted = false;
     432           0 :       this._downloads[fileKey]!.requested = DateTime.now();
     433           0 :       this._downloads[fileKey]!.markUpdate();
     434           0 :       notifyListeners();
     435             :     }
     436             :   }
     437             : 
     438           0 :   double downloadProgress(String fileKey) {
     439           0 :     return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0;
     440             :   }
     441             : 
     442             :   // used for loading interrupted download info; use downloadMarkFinished for successful downloads
     443           0 :   void downloadSetPath(String fileKey, String path) {
     444           0 :     if (this._downloads.containsKey(fileKey)) {
     445           0 :       this._downloads[fileKey]!.downloadedTo = path;
     446           0 :       notifyListeners();
     447             :     }
     448             :   }
     449             : 
     450             :   // set the download path for the sender
     451           0 :   void downloadSetPathForSender(String fileKey, String path) {
     452             :     // we may trigger this event for auto-downloaded receivers too,
     453             :     // as such we don't assume anything else about the file...other than that
     454             :     // it exists.
     455           0 :     if (!this._downloads.containsKey(fileKey)) {
     456             :       // this will be overwritten by download update if the file is being downloaded
     457           0 :       this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now());
     458             :     }
     459           0 :     this._downloads[fileKey]!.downloadedTo = path;
     460           0 :     notifyListeners();
     461             :   }
     462             : 
     463           0 :   String? downloadFinalPath(String fileKey) {
     464           0 :     return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null;
     465             :   }
     466             : 
     467           0 :   String downloadSpeed(String fileKey) {
     468           0 :     if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) {
     469             :       return "0 B/s";
     470             :     }
     471           0 :     var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096;
     472           0 :     var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds;
     473           0 :     if (seconds == 0) {
     474             :       return "0 B/s";
     475             :     }
     476           0 :     return prettyBytes((bytes / seconds).round()) + "/s";
     477             :   }
     478             : 
     479           0 :   void waitForDownloadComplete(int identifier, String fileKey) {
     480           0 :     _downloadTriggers[fileKey] = identifier;
     481           0 :     notifyListeners();
     482             :   }
     483             : 
     484           0 :   int cacheMemUsage() {
     485           0 :     return _contacts.cacheMemUsage();
     486             :   }
     487             : 
     488           0 :   void downloadReset(String fileKey) {
     489           0 :     this._downloads.remove(fileKey);
     490           0 :     notifyListeners();
     491             :   }
     492             : 
     493           0 :   String getPrivateName() {
     494           0 :     return _privateName;
     495             :   }
     496             : 
     497           0 :   void setPrivateName(String pn) {
     498           0 :     _privateName = pn;
     499           0 :     notifyListeners();
     500             :   }
     501             : 
     502             :   // Profile Attributes. Can be set in Profile Edit View...
     503             :   List<String?> attributes = [null, null, null];
     504           0 :   void setAttribute(int i, String? value) {
     505           0 :     this.attributes[i] = value;
     506           0 :     notifyListeners();
     507             :   }
     508             : 
     509             :   ProfileStatusMenu availabilityStatus = ProfileStatusMenu.available;
     510           0 :   void setAvailabilityStatus(String status) {
     511             :     switch (status) {
     512           0 :       case "available":
     513           0 :         availabilityStatus = ProfileStatusMenu.available;
     514             :         break;
     515           0 :       case "busy":
     516           0 :         availabilityStatus = ProfileStatusMenu.busy;
     517             :         break;
     518           0 :       case "away":
     519           0 :         availabilityStatus = ProfileStatusMenu.away;
     520             :         break;
     521             :       default:
     522             :         ProfileStatusMenu.available;
     523             :     }
     524           0 :     notifyListeners();
     525             :   }
     526             : 
     527           0 :   Color getBorderColor(OpaqueThemeType theme) {
     528           0 :     switch (this.availabilityStatus) {
     529           0 :       case ProfileStatusMenu.available:
     530           0 :         return theme.portraitOnlineBorderColor;
     531           0 :       case ProfileStatusMenu.away:
     532           0 :         return theme.portraitOnlineAwayColor;
     533           0 :       case ProfileStatusMenu.busy:
     534           0 :         return theme.portraitOnlineBusyColor;
     535             :       default:
     536           0 :         throw UnimplementedError("not a valid status");
     537             :     }
     538             :   }
     539             : 
     540             :   // during deactivation it is possible that the event bus is cleaned up prior to statuses being updated
     541             :   // this method nicely cleans up our current state so that the UI functions as expected.
     542             :   // FIXME: Cwtch should be sending these events prior to shutting down the engine...
     543           0 :   void deactivatePeerEngine(BuildContext context) {
     544           0 :     Provider.of<FlwtchState>(context, listen: false).cwtch.DeactivatePeerEngine(onion);
     545           0 :     this.contactList.contacts.forEach((element) {
     546           0 :       element.status = "Disconnected";
     547             :       // reset retry time to allow for instant reconnection...
     548           0 :       element.lastRetryTime = element.loaded;
     549             :     });
     550           0 :     this.serverList.servers.forEach((element) {
     551           0 :       element.status = "Disconnected";
     552             :     });
     553             :   }
     554             : }

Generated by: LCOV version 1.14