From 706c1fb3540c22d21fa9a1a2ca7110cf52ad5b74 Mon Sep 17 00:00:00 2001 From: Dan Ballard Date: Tue, 18 Jan 2022 16:26:52 -0500 Subject: [PATCH] move all classes in model.dart to their own models/X.dart --- lib/cwtch/cwtchNotifier.dart | 4 +- lib/main.dart | 3 +- lib/model.dart | 751 ------------------------- lib/models/appstate.dart | 71 +++ lib/models/chatmessage.dart | 15 + lib/models/contact.dart | 214 +++++++ lib/models/contactlist.dart | 125 ++++ lib/models/filedownloadprogress.dart | 27 + lib/models/message.dart | 2 +- lib/models/messagecache.dart | 7 + lib/models/messages/filemessage.dart | 2 +- lib/models/messages/invitemessage.dart | 2 +- lib/models/messages/quotedmessage.dart | 1 - lib/models/messages/textmessage.dart | 2 - lib/models/profile.dart | 272 +++++++++ lib/models/profilelist.dart | 30 + lib/models/profileservers.dart | 3 +- lib/views/addcontactview.dart | 2 +- lib/views/addeditprofileview.dart | 2 +- lib/views/contactsview.dart | 6 +- lib/views/doublecolview.dart | 4 +- lib/views/groupsettingsview.dart | 4 +- lib/views/messageview.dart | 5 +- lib/views/peersettingsview.dart | 3 +- lib/views/profilemgrview.dart | 4 +- lib/views/profileserversview.dart | 2 +- lib/views/remoteserverview.dart | 3 +- lib/views/splashView.dart | 2 +- lib/widgets/DropdownContacts.dart | 3 +- lib/widgets/contactrow.dart | 4 +- lib/widgets/filebubble.dart | 4 +- lib/widgets/invitationbubble.dart | 3 +- lib/widgets/messagebubble.dart | 3 +- lib/widgets/messagelist.dart | 4 +- lib/widgets/messagerow.dart | 4 +- lib/widgets/profilerow.dart | 4 +- lib/widgets/quotedmessage.dart | 3 +- lib/widgets/remoteserverrow.dart | 2 +- lib/widgets/serverrow.dart | 1 - 39 files changed, 820 insertions(+), 783 deletions(-) delete mode 100644 lib/model.dart create mode 100644 lib/models/appstate.dart create mode 100644 lib/models/chatmessage.dart create mode 100644 lib/models/contact.dart create mode 100644 lib/models/contactlist.dart create mode 100644 lib/models/filedownloadprogress.dart create mode 100644 lib/models/messagecache.dart create mode 100644 lib/models/profile.dart create mode 100644 lib/models/profilelist.dart diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index f400f910..ef270f90 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -1,6 +1,9 @@ import 'dart:convert'; import 'package:cwtch/main.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profilelist.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/notification_manager.dart'; @@ -10,7 +13,6 @@ import 'package:cwtch/torstatus.dart'; import '../config.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; // Class that handles libcwtch-go events (received either via ffi with an isolate or gomobile over a method channel from kotlin) diff --git a/lib/main.dart b/lib/main.dart index 5d307a16..664464e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,7 +15,8 @@ import 'package:provider/provider.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; import 'licenses.dart'; -import 'model.dart'; +import 'models/appstate.dart'; +import 'models/profilelist.dart'; import 'models/servers.dart'; import 'views/profilemgrview.dart'; import 'views/splashView.dart'; diff --git a/lib/model.dart b/lib/model.dart deleted file mode 100644 index 00ebba2e..00000000 --- a/lib/model.dart +++ /dev/null @@ -1,751 +0,0 @@ -import 'dart:convert'; - -import 'package:cwtch/config.dart'; -import 'package:cwtch/models/message.dart'; -import 'package:cwtch/widgets/messagerow.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:cwtch/models/profileservers.dart'; - -//////////////////// -/// UI State /// -//////////////////// - -class ChatMessage { - final int o; - final String d; - - ChatMessage({required this.o, required this.d}); - - ChatMessage.fromJson(Map json) - : o = json['o'], - d = json['d']; - - Map toJson() => { - 'o': o, - 'd': d, - }; -} - -enum ModalState { none, storageMigration } - -class AppState extends ChangeNotifier { - bool cwtchInit = false; - ModalState modalState = ModalState.none; - bool cwtchIsClosing = false; - String appError = ""; - String? _selectedProfile; - int? _selectedConversation; - int _initialScrollIndex = 0; - int _hoveredIndex = -1; - int? _selectedIndex; - bool _unreadMessagesBelow = false; - - void SetCwtchInit() { - cwtchInit = true; - notifyListeners(); - } - - void SetAppError(String error) { - appError = error; - notifyListeners(); - } - - void SetModalState(ModalState newState) { - modalState = newState; - notifyListeners(); - } - - String? get selectedProfile => _selectedProfile; - set selectedProfile(String? newVal) { - this._selectedProfile = newVal; - notifyListeners(); - } - - int? get selectedConversation => _selectedConversation; - set selectedConversation(int? newVal) { - this._selectedConversation = newVal; - notifyListeners(); - } - - int? get selectedIndex => _selectedIndex; - set selectedIndex(int? newVal) { - this._selectedIndex = newVal; - notifyListeners(); - } - - // Never use this for message lookup - can be a non-indexed value - // e.g. -1 - int get hoveredIndex => _hoveredIndex; - set hoveredIndex(int newVal) { - this._hoveredIndex = newVal; - notifyListeners(); - } - - bool get unreadMessagesBelow => _unreadMessagesBelow; - set unreadMessagesBelow(bool newVal) { - this._unreadMessagesBelow = newVal; - notifyListeners(); - } - - int get initialScrollIndex => _initialScrollIndex; - set initialScrollIndex(int newVal) { - this._initialScrollIndex = newVal; - notifyListeners(); - } - - bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; -} - -/////////////////// -/// Providers /// -/////////////////// - -class ProfileListState extends ChangeNotifier { - List _profiles = []; - int get num => _profiles.length; - - void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { - var idx = _profiles.indexWhere((element) => element.onion == onion); - if (idx == -1) { - _profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); - } else { - _profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); - } - notifyListeners(); - } - - List get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier - - ProfileInfoState? getProfile(String onion) { - int idx = _profiles.indexWhere((element) => element.onion == onion); - return idx >= 0 ? _profiles[idx] : null; - } - - void delete(String onion) { - _profiles.removeWhere((element) => element.onion == onion); - notifyListeners(); - } -} - -class ContactListState extends ChangeNotifier { - ProfileServerListState? servers; - List _contacts = []; - String _filter = ""; - int get num => _contacts.length; - int get numFiltered => isFiltered ? filteredList().length : num; - bool get isFiltered => _filter != ""; - String get filter => _filter; - set filter(String newVal) { - _filter = newVal.toLowerCase(); - notifyListeners(); - } - - void connectServers(ProfileServerListState servers) { - this.servers = servers; - } - - List filteredList() { - if (!isFiltered) return contacts; - return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); - } - - void addAll(Iterable newContacts) { - _contacts.addAll(newContacts); - servers?.clearGroups(); - _contacts.forEach((contact) { - if (contact.isGroup) { - servers?.addGroup(contact); - } - }); - resort(); - notifyListeners(); - } - - void add(ContactInfoState newContact) { - _contacts.add(newContact); - if (newContact.isGroup) { - servers?.addGroup(newContact); - } - resort(); - notifyListeners(); - } - - void resort() { - _contacts.sort((ContactInfoState a, ContactInfoState b) { - // return -1 = a first in list - // return 1 = b first in list - - // blocked contacts last - if (a.isBlocked == true && b.isBlocked != true) return 1; - if (a.isBlocked != true && b.isBlocked == true) return -1; - // archive is next... - if (!a.isArchived && b.isArchived) return -1; - if (a.isArchived && !b.isArchived) return 1; - - // unapproved top - if (a.isInvitation && !b.isInvitation) return -1; - if (!a.isInvitation && b.isInvitation) return 1; - - // special sorting for contacts with no messages in either history - if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) { - // online contacts first - if (a.isOnline() && !b.isOnline()) return -1; - if (!a.isOnline() && b.isOnline()) return 1; - // finally resort to onion - return a.onion.toString().compareTo(b.onion.toString()); - } - // finally... most recent history first - if (a.lastMessageTime.millisecondsSinceEpoch == 0) return 1; - if (b.lastMessageTime.millisecondsSinceEpoch == 0) return -1; - return b.lastMessageTime.compareTo(a.lastMessageTime); - }); - // if(changed) { - notifyListeners(); - //} - } - - void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) { - var contact = getContact(forIdentifier); - if (contact == null) return; - - // Assert that the new time is after the current last message time AND that - // new message time is before the current time. - if (newMessageTime.isAfter(contact.lastMessageTime)) { - if (newMessageTime.isBefore(DateTime.now().toLocal())) { - contact.lastMessageTime = newMessageTime; - } else { - // Otherwise set the last message time to now... - contact.lastMessageTime = DateTime.now().toLocal(); - } - resort(); - } - } - - List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier - - ContactInfoState? getContact(int identifier) { - int idx = _contacts.indexWhere((element) => element.identifier == identifier); - return idx >= 0 ? _contacts[idx] : null; - } - - void removeContact(int identifier) { - int idx = _contacts.indexWhere((element) => element.identifier == identifier); - if (idx >= 0) { - _contacts.removeAt(idx); - notifyListeners(); - } - } - - ContactInfoState? findContact(String byHandle) { - int idx = _contacts.indexWhere((element) => element.onion == byHandle); - return idx >= 0 ? _contacts[idx] : null; - } -} - -class ProfileInfoState extends ChangeNotifier { - ProfileServerListState _servers = ProfileServerListState(); - ContactListState _contacts = ContactListState(); - final String onion; - String _nickname = ""; - String _imagePath = ""; - int _unreadMessages = 0; - bool _online = false; - Map _downloads = Map(); - - // assume profiles are encrypted...this will be set to false - // in the constructor if the profile is encrypted with the defacto password. - bool _encrypted = true; - - ProfileInfoState({ - required this.onion, - nickname = "", - imagePath = "", - unreadMessages = 0, - contactsJson = "", - serversJson = "", - online = false, - encrypted = true, - }) { - this._nickname = nickname; - this._imagePath = imagePath; - this._unreadMessages = unreadMessages; - this._online = online; - this._encrypted = encrypted; - - _contacts.connectServers(this._servers); - - if (contactsJson != null && contactsJson != "" && contactsJson != "null") { - this.replaceServers(serversJson); - - List contacts = jsonDecode(contactsJson); - this._contacts.addAll(contacts.map((contact) { - return ContactInfoState(this.onion, contact["identifier"], contact["onion"], - nickname: contact["name"], - status: contact["status"], - imagePath: contact["picture"], - accepted: contact["accepted"], - blocked: contact["blocked"], - savePeerHistory: contact["saveConversationHistory"], - numMessages: contact["numMessages"], - numUnread: contact["numUnread"], - isGroup: contact["isGroup"], - server: contact["groupServer"], - archived: contact["isArchived"] == true, - lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]))); - })); - - // dummy set to invoke sort-on-load - if (this._contacts.num > 0) { - this._contacts.updateLastMessageTime(this._contacts._contacts.first.identifier, this._contacts._contacts.first.lastMessageTime); - } - } - } - - // Parse out the server list json into our server info state struct... - void replaceServers(String serversJson) { - if (serversJson != "" && serversJson != "null") { - List servers = jsonDecode(serversJson); - this._servers.replace(servers.map((server) { - // TODO Keys... - return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); - })); - - this._contacts.contacts.forEach((contact) { - if (contact.isGroup) { - _servers.addGroup(contact); - } - }); - - notifyListeners(); - } - } - - // - void updateServerStatusCache(String server, String status) { - this._servers.updateServerState(server, status); - notifyListeners(); - } - - // Getters and Setters for Online Status - bool get isOnline => this._online; - set isOnline(bool newValue) { - this._online = newValue; - notifyListeners(); - } - - // Check encrypted status for profile info screen - bool get isEncrypted => this._encrypted; - - String get nickname => this._nickname; - set nickname(String newValue) { - this._nickname = newValue; - notifyListeners(); - } - - String get imagePath => this._imagePath; - set imagePath(String newVal) { - this._imagePath = newVal; - notifyListeners(); - } - - int get unreadMessages => this._unreadMessages; - set unreadMessages(int newVal) { - this._unreadMessages = newVal; - notifyListeners(); - } - - // Remove a contact from a list. Currently only used when rejecting a group invitation. - // Eventually will also be used for other removals. - void removeContact(String handle) { - int idx = this.contactList._contacts.indexWhere((element) => element.onion == handle); - this.contactList._contacts.removeAt(idx); - notifyListeners(); - } - - ContactListState get contactList => this._contacts; - ProfileServerListState get serverList => this._servers; - - @override - void dispose() { - super.dispose(); - print("profileinfostate.dispose()"); - } - - void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) { - this._nickname = name; - this._imagePath = picture; - this._online = online; - this.replaceServers(serverJson); - - if (contactsJson != null && contactsJson != "" && contactsJson != "null") { - List contacts = jsonDecode(contactsJson); - contacts.forEach((contact) { - var profileContact = this._contacts.getContact(contact["identifier"]); - if (profileContact != null) { - profileContact.status = contact["status"]; - profileContact.totalMessages = contact["numMessages"]; - profileContact.unreadMessages = contact["numUnread"]; - profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])); - } else { - this._contacts.add(ContactInfoState( - this.onion, - contact["identifier"], - contact["onion"], - nickname: contact["name"], - status: contact["status"], - imagePath: contact["picture"], - accepted: contact["accepted"], - blocked: contact["blocked"], - savePeerHistory: contact["saveConversationHistory"], - numMessages: contact["numMessages"], - numUnread: contact["numUnread"], - isGroup: contact["isGroup"], - server: contact["groupServer"], - lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), - )); - } - }); - } - this._contacts.resort(); - } - - void downloadInit(String fileKey, int numChunks) { - this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); - } - - void downloadUpdate(String fileKey, int progress, int numChunks) { - if (!downloadActive(fileKey)) { - this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); - if (progress < 0) { - this._downloads[fileKey]!.interrupted = true; - } - } else { - if (this._downloads[fileKey]!.interrupted) { - this._downloads[fileKey]!.interrupted = false; - } - this._downloads[fileKey]!.chunksDownloaded = progress; - this._downloads[fileKey]!.chunksTotal = numChunks; - } - notifyListeners(); - } - - void downloadMarkManifest(String fileKey) { - if (!downloadActive(fileKey)) { - this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); - } - this._downloads[fileKey]!.gotManifest = true; - notifyListeners(); - } - - void downloadMarkFinished(String fileKey, String finalPath) { - if (!downloadActive(fileKey)) { - // happens as a result of a CheckDownloadStatus call, - // invoked from a historical (timeline) download message - // so setting numChunks correctly shouldn't matter - this.downloadInit(fileKey, 1); - } - // only update if different - if (!this._downloads[fileKey]!.complete) { - this._downloads[fileKey]!.timeEnd = DateTime.now(); - this._downloads[fileKey]!.downloadedTo = finalPath; - this._downloads[fileKey]!.complete = true; - notifyListeners(); - } - } - - bool downloadKnown(String fileKey) { - return this._downloads.containsKey(fileKey); - } - - bool downloadActive(String fileKey) { - return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted; - } - - bool downloadGotManifest(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest; - } - - bool downloadComplete(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; - } - - bool downloadInterrupted(String fileKey) { - return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted; - } - - void downloadMarkResumed(String fileKey) { - if (this._downloads.containsKey(fileKey)) { - this._downloads[fileKey]!.interrupted = false; - } - } - - double downloadProgress(String fileKey) { - return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; - } - - // used for loading interrupted download info; use downloadMarkFinished for successful downloads - void downloadSetPath(String fileKey, String path) { - if (this._downloads.containsKey(fileKey)) { - this._downloads[fileKey]!.downloadedTo = path; - } - } - - String? downloadFinalPath(String fileKey) { - return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; - } - - String downloadSpeed(String fileKey) { - if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { - return "0 B/s"; - } - var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; - var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds; - if (seconds == 0) { - return "0 B/s"; - } - return prettyBytes((bytes / seconds).round()) + "/s"; - } -} - -class FileDownloadProgress { - int chunksDownloaded = 0; - int chunksTotal = 1; - bool complete = false; - bool gotManifest = false; - bool interrupted = false; - String? downloadedTo; - DateTime? timeStart; - DateTime? timeEnd; - - FileDownloadProgress(this.chunksTotal, this.timeStart); - double progress() { - return 1.0 * chunksDownloaded / chunksTotal; - } -} - -String prettyBytes(int bytes) { - if (bytes > 1000000000) { - return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB"; - } else if (bytes > 1000000) { - return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB"; - } else if (bytes > 1000) { - return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB"; - } else { - return bytes.toString() + " B"; - } -} - -class MessageCache { - final MessageMetadata metadata; - final String wrapper; - MessageCache(this.metadata, this.wrapper); -} - -class ContactInfoState extends ChangeNotifier { - final String profileOnion; - final int identifier; - final String onion; - late String _nickname; - - late bool _accepted; - late bool _blocked; - late String _status; - late String _imagePath; - late String _savePeerHistory; - late int _unreadMessages = 0; - late int _totalMessages = 0; - late DateTime _lastMessageTime; - late Map> keys; - late List messageCache; - int _newMarker = 0; - DateTime _newMarkerClearAt = DateTime.now(); - - // todo: a nicer way to model contacts, groups and other "entities" - late bool _isGroup; - String? _server; - late bool _archived; - - String? _acnCircuit; - - ContactInfoState(this.profileOnion, this.identifier, this.onion, - {nickname = "", - isGroup = false, - accepted = false, - blocked = false, - status = "", - imagePath = "", - savePeerHistory = "DeleteHistoryConfirmed", - numMessages = 0, - numUnread = 0, - lastMessageTime, - server, - archived = false}) { - this._nickname = nickname; - this._isGroup = isGroup; - this._accepted = accepted; - this._blocked = blocked; - this._status = status; - this._imagePath = imagePath; - this._totalMessages = numMessages; - this._unreadMessages = numUnread; - this._savePeerHistory = savePeerHistory; - this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; - this._server = server; - this._archived = archived; - this.messageCache = List.empty(growable: true); - keys = Map>(); - } - - String get nickname => this._nickname; - - String get savePeerHistory => this._savePeerHistory; - - String? get acnCircuit => this._acnCircuit; - set acnCircuit(String? acnCircuit) { - this._acnCircuit = acnCircuit; - notifyListeners(); - } - - // Indicated whether the conversation is archived, in which case it will - // be moved to the very bottom of the active conversations list until - // new messages appear - set isArchived(bool archived) { - this._archived = archived; - notifyListeners(); - } - - bool get isArchived => this._archived; - - set savePeerHistory(String newVal) { - this._savePeerHistory = newVal; - notifyListeners(); - } - - set nickname(String newVal) { - this._nickname = newVal; - notifyListeners(); - } - - bool get isGroup => this._isGroup; - set isGroup(bool newVal) { - this._isGroup = newVal; - notifyListeners(); - } - - bool get isBlocked => this._blocked; - - bool get isInvitation => !this._blocked && !this._accepted; - - set accepted(bool newVal) { - this._accepted = newVal; - notifyListeners(); - } - - set blocked(bool newVal) { - this._blocked = newVal; - notifyListeners(); - } - - String get status => this._status; - set status(String newVal) { - this._status = newVal; - notifyListeners(); - } - - int get unreadMessages => this._unreadMessages; - set unreadMessages(int newVal) { - // don't reset newMarker position when unreadMessages is being cleared - if (newVal > 0) { - this._newMarker = newVal; - } else { - this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); - } - this._unreadMessages = newVal; - notifyListeners(); - } - - int get newMarker { - if (DateTime.now().isAfter(this._newMarkerClearAt)) { - // perform heresy - this._newMarker = 0; - // no need to notifyListeners() because presumably this getter is - // being called from a renderer anyway - } - return this._newMarker; - } - - // what's a getter that sometimes sets without a setter - // that sometimes doesn't set - set newMarker(int newVal) { - // only unreadMessages++ can set newMarker = 1; - // avoids drawing a marker when the convo is already open - if (newVal >= 1) { - this._newMarker = newVal; - notifyListeners(); - } - } - - int get totalMessages => this._totalMessages; - set totalMessages(int newVal) { - this._totalMessages = newVal; - notifyListeners(); - } - - String get imagePath => this._imagePath; - set imagePath(String newVal) { - this._imagePath = newVal; - notifyListeners(); - } - - DateTime get lastMessageTime => this._lastMessageTime; - set lastMessageTime(DateTime newVal) { - this._lastMessageTime = newVal; - notifyListeners(); - } - - // we only allow callers to fetch the server - get server => this._server; - - bool isOnline() { - if (this.isGroup == true) { - // We now have an out of sync warning so we will mark these as online... - return this.status == "Authenticated" || this.status == "Synced"; - } else { - return this.status == "Authenticated"; - } - } - - GlobalKey getMessageKey(int conversation, int message) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); - if (keys[index] == null) { - keys[index] = GlobalKey(); - } - GlobalKey ret = keys[index]!; - return ret; - } - - GlobalKey? getMessageKeyOrFail(int conversation, int message) { - String index = "c: " + conversation.toString() + " m:" + message.toString(); - - if (keys[index] == null) { - return null; - } - GlobalKey ret = keys[index]!; - return ret; - } - - void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { - this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); - this.totalMessages += 1; - } - - void bumpMessageCache() { - this.messageCache.insert(0, null); - this.totalMessages += 1; - } - - void ackCache(int messageID) { - this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; - notifyListeners(); - } -} diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart new file mode 100644 index 00000000..39ae8454 --- /dev/null +++ b/lib/models/appstate.dart @@ -0,0 +1,71 @@ +import 'package:flutter/widgets.dart'; + +enum ModalState { none, storageMigration } + +class AppState extends ChangeNotifier { + bool cwtchInit = false; + ModalState modalState = ModalState.none; + bool cwtchIsClosing = false; + String appError = ""; + String? _selectedProfile; + int? _selectedConversation; + int _initialScrollIndex = 0; + int _hoveredIndex = -1; + int? _selectedIndex; + bool _unreadMessagesBelow = false; + + void SetCwtchInit() { + cwtchInit = true; + notifyListeners(); + } + + void SetAppError(String error) { + appError = error; + notifyListeners(); + } + + void SetModalState(ModalState newState) { + modalState = newState; + notifyListeners(); + } + + String? get selectedProfile => _selectedProfile; + set selectedProfile(String? newVal) { + this._selectedProfile = newVal; + notifyListeners(); + } + + int? get selectedConversation => _selectedConversation; + set selectedConversation(int? newVal) { + this._selectedConversation = newVal; + notifyListeners(); + } + + int? get selectedIndex => _selectedIndex; + set selectedIndex(int? newVal) { + this._selectedIndex = newVal; + notifyListeners(); + } + + // Never use this for message lookup - can be a non-indexed value + // e.g. -1 + int get hoveredIndex => _hoveredIndex; + set hoveredIndex(int newVal) { + this._hoveredIndex = newVal; + notifyListeners(); + } + + bool get unreadMessagesBelow => _unreadMessagesBelow; + set unreadMessagesBelow(bool newVal) { + this._unreadMessagesBelow = newVal; + notifyListeners(); + } + + int get initialScrollIndex => _initialScrollIndex; + set initialScrollIndex(int newVal) { + this._initialScrollIndex = newVal; + notifyListeners(); + } + + bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height; +} \ No newline at end of file diff --git a/lib/models/chatmessage.dart b/lib/models/chatmessage.dart new file mode 100644 index 00000000..f45d9e95 --- /dev/null +++ b/lib/models/chatmessage.dart @@ -0,0 +1,15 @@ +class ChatMessage { + final int o; + final String d; + + ChatMessage({required this.o, required this.d}); + + ChatMessage.fromJson(Map json) + : o = json['o'], + d = json['d']; + + Map toJson() => { + 'o': o, + 'd': d, + }; +} \ No newline at end of file diff --git a/lib/models/contact.dart b/lib/models/contact.dart new file mode 100644 index 00000000..0b840b5c --- /dev/null +++ b/lib/models/contact.dart @@ -0,0 +1,214 @@ +import 'package:cwtch/widgets/messagerow.dart'; +import 'package:flutter/widgets.dart'; + +import 'message.dart'; +import 'messagecache.dart'; + +class ContactInfoState extends ChangeNotifier { + final String profileOnion; + final int identifier; + final String onion; + late String _nickname; + + late bool _accepted; + late bool _blocked; + late String _status; + late String _imagePath; + late String _savePeerHistory; + late int _unreadMessages = 0; + late int _totalMessages = 0; + late DateTime _lastMessageTime; + late Map> keys; + late List messageCache; + int _newMarker = 0; + DateTime _newMarkerClearAt = DateTime.now(); + + // todo: a nicer way to model contacts, groups and other "entities" + late bool _isGroup; + String? _server; + late bool _archived; + + String? _acnCircuit; + + ContactInfoState(this.profileOnion, this.identifier, this.onion, + {nickname = "", + isGroup = false, + accepted = false, + blocked = false, + status = "", + imagePath = "", + savePeerHistory = "DeleteHistoryConfirmed", + numMessages = 0, + numUnread = 0, + lastMessageTime, + server, + archived = false}) { + this._nickname = nickname; + this._isGroup = isGroup; + this._accepted = accepted; + this._blocked = blocked; + this._status = status; + this._imagePath = imagePath; + this._totalMessages = numMessages; + this._unreadMessages = numUnread; + this._savePeerHistory = savePeerHistory; + this._lastMessageTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime; + this._server = server; + this._archived = archived; + this.messageCache = List.empty(growable: true); + keys = Map>(); + } + + String get nickname => this._nickname; + + String get savePeerHistory => this._savePeerHistory; + + String? get acnCircuit => this._acnCircuit; + set acnCircuit(String? acnCircuit) { + this._acnCircuit = acnCircuit; + notifyListeners(); + } + + // Indicated whether the conversation is archived, in which case it will + // be moved to the very bottom of the active conversations list until + // new messages appear + set isArchived(bool archived) { + this._archived = archived; + notifyListeners(); + } + + bool get isArchived => this._archived; + + set savePeerHistory(String newVal) { + this._savePeerHistory = newVal; + notifyListeners(); + } + + set nickname(String newVal) { + this._nickname = newVal; + notifyListeners(); + } + + bool get isGroup => this._isGroup; + set isGroup(bool newVal) { + this._isGroup = newVal; + notifyListeners(); + } + + bool get isBlocked => this._blocked; + + bool get isInvitation => !this._blocked && !this._accepted; + + set accepted(bool newVal) { + this._accepted = newVal; + notifyListeners(); + } + + set blocked(bool newVal) { + this._blocked = newVal; + notifyListeners(); + } + + String get status => this._status; + set status(String newVal) { + this._status = newVal; + notifyListeners(); + } + + int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + // don't reset newMarker position when unreadMessages is being cleared + if (newVal > 0) { + this._newMarker = newVal; + } else { + this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); + } + this._unreadMessages = newVal; + notifyListeners(); + } + + int get newMarker { + if (DateTime.now().isAfter(this._newMarkerClearAt)) { + // perform heresy + this._newMarker = 0; + // no need to notifyListeners() because presumably this getter is + // being called from a renderer anyway + } + return this._newMarker; + } + + // what's a getter that sometimes sets without a setter + // that sometimes doesn't set + set newMarker(int newVal) { + // only unreadMessages++ can set newMarker = 1; + // avoids drawing a marker when the convo is already open + if (newVal >= 1) { + this._newMarker = newVal; + notifyListeners(); + } + } + + int get totalMessages => this._totalMessages; + set totalMessages(int newVal) { + this._totalMessages = newVal; + notifyListeners(); + } + + String get imagePath => this._imagePath; + set imagePath(String newVal) { + this._imagePath = newVal; + notifyListeners(); + } + + DateTime get lastMessageTime => this._lastMessageTime; + set lastMessageTime(DateTime newVal) { + this._lastMessageTime = newVal; + notifyListeners(); + } + + // we only allow callers to fetch the server + get server => this._server; + + bool isOnline() { + if (this.isGroup == true) { + // We now have an out of sync warning so we will mark these as online... + return this.status == "Authenticated" || this.status == "Synced"; + } else { + return this.status == "Authenticated"; + } + } + + GlobalKey getMessageKey(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); + if (keys[index] == null) { + keys[index] = GlobalKey(); + } + GlobalKey ret = keys[index]!; + return ret; + } + + GlobalKey? getMessageKeyOrFail(int conversation, int message) { + String index = "c: " + conversation.toString() + " m:" + message.toString(); + + if (keys[index] == null) { + return null; + } + GlobalKey ret = keys[index]!; + return ret; + } + + void updateMessageCache(int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data) { + this.messageCache.insert(0, MessageCache(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data)); + this.totalMessages += 1; + } + + void bumpMessageCache() { + this.messageCache.insert(0, null); + this.totalMessages += 1; + } + + void ackCache(int messageID) { + this.messageCache.firstWhere((element) => element?.metadata.messageID == messageID)?.metadata.ackd = true; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart new file mode 100644 index 00000000..d61038a3 --- /dev/null +++ b/lib/models/contactlist.dart @@ -0,0 +1,125 @@ +import 'package:flutter/widgets.dart'; + +import 'contact.dart'; +import 'profileservers.dart'; + +class ContactListState extends ChangeNotifier { + ProfileServerListState? servers; + List _contacts = []; + String _filter = ""; + int get num => _contacts.length; + int get numFiltered => isFiltered ? filteredList().length : num; + bool get isFiltered => _filter != ""; + String get filter => _filter; + set filter(String newVal) { + _filter = newVal.toLowerCase(); + notifyListeners(); + } + + void connectServers(ProfileServerListState servers) { + this.servers = servers; + } + + List filteredList() { + if (!isFiltered) return contacts; + return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList(); + } + + void addAll(Iterable newContacts) { + _contacts.addAll(newContacts); + servers?.clearGroups(); + _contacts.forEach((contact) { + if (contact.isGroup) { + servers?.addGroup(contact); + } + }); + resort(); + notifyListeners(); + } + + void add(ContactInfoState newContact) { + _contacts.add(newContact); + if (newContact.isGroup) { + servers?.addGroup(newContact); + } + resort(); + notifyListeners(); + } + + void resort() { + _contacts.sort((ContactInfoState a, ContactInfoState b) { + // return -1 = a first in list + // return 1 = b first in list + + // blocked contacts last + if (a.isBlocked == true && b.isBlocked != true) return 1; + if (a.isBlocked != true && b.isBlocked == true) return -1; + // archive is next... + if (!a.isArchived && b.isArchived) return -1; + if (a.isArchived && !b.isArchived) return 1; + + // unapproved top + if (a.isInvitation && !b.isInvitation) return -1; + if (!a.isInvitation && b.isInvitation) return 1; + + // special sorting for contacts with no messages in either history + if (a.lastMessageTime.millisecondsSinceEpoch == 0 && b.lastMessageTime.millisecondsSinceEpoch == 0) { + // online contacts first + if (a.isOnline() && !b.isOnline()) return -1; + if (!a.isOnline() && b.isOnline()) return 1; + // finally resort to onion + return a.onion.toString().compareTo(b.onion.toString()); + } + // finally... most recent history first + if (a.lastMessageTime.millisecondsSinceEpoch == 0) return 1; + if (b.lastMessageTime.millisecondsSinceEpoch == 0) return -1; + return b.lastMessageTime.compareTo(a.lastMessageTime); + }); + // if(changed) { + notifyListeners(); + //} + } + + void updateLastMessageTime(int forIdentifier, DateTime newMessageTime) { + var contact = getContact(forIdentifier); + if (contact == null) return; + + // Assert that the new time is after the current last message time AND that + // new message time is before the current time. + if (newMessageTime.isAfter(contact.lastMessageTime)) { + if (newMessageTime.isBefore(DateTime.now().toLocal())) { + contact.lastMessageTime = newMessageTime; + } else { + // Otherwise set the last message time to now... + contact.lastMessageTime = DateTime.now().toLocal(); + } + resort(); + } + } + + List get contacts => _contacts.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + + ContactInfoState? getContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); + return idx >= 0 ? _contacts[idx] : null; + } + + void removeContact(int identifier) { + int idx = _contacts.indexWhere((element) => element.identifier == identifier); + if (idx >= 0) { + _contacts.removeAt(idx); + notifyListeners(); + } + } + + void removeContactByHandle(String handle) { + int idx = _contacts.indexWhere((element) => element.onion == handle); + _contacts.removeAt(idx); + notifyListeners(); + } + + ContactInfoState? findContact(String byHandle) { + int idx = _contacts.indexWhere((element) => element.onion == byHandle); + return idx >= 0 ? _contacts[idx] : null; + } +} \ No newline at end of file diff --git a/lib/models/filedownloadprogress.dart b/lib/models/filedownloadprogress.dart new file mode 100644 index 00000000..ea5c279a --- /dev/null +++ b/lib/models/filedownloadprogress.dart @@ -0,0 +1,27 @@ +class FileDownloadProgress { + int chunksDownloaded = 0; + int chunksTotal = 1; + bool complete = false; + bool gotManifest = false; + bool interrupted = false; + String? downloadedTo; + DateTime? timeStart; + DateTime? timeEnd; + + FileDownloadProgress(this.chunksTotal, this.timeStart); + double progress() { + return 1.0 * chunksDownloaded / chunksTotal; + } +} + +String prettyBytes(int bytes) { + if (bytes > 1000000000) { + return (1.0 * bytes / 1000000000).toStringAsFixed(1) + " GB"; + } else if (bytes > 1000000) { + return (1.0 * bytes / 1000000).toStringAsFixed(1) + " MB"; + } else if (bytes > 1000) { + return (1.0 * bytes / 1000).toStringAsFixed(1) + " kB"; + } else { + return bytes.toString() + " B"; + } +} \ No newline at end of file diff --git a/lib/models/message.dart b/lib/models/message.dart index dc4c5b97..91701cc8 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -4,12 +4,12 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'messages/filemessage.dart'; import 'messages/invitemessage.dart'; import 'messages/malformedmessage.dart'; import 'messages/quotedmessage.dart'; import 'messages/textmessage.dart'; +import 'profile.dart'; // Define the overlays const TextMessageOverlay = 1; diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart new file mode 100644 index 00000000..8f9d5e16 --- /dev/null +++ b/lib/models/messagecache.dart @@ -0,0 +1,7 @@ +import 'message.dart'; + +class MessageCache { + final MessageMetadata metadata; + final String wrapper; + MessageCache(this.metadata, this.wrapper); +} \ No newline at end of file diff --git a/lib/models/messages/filemessage.dart b/lib/models/messages/filemessage.dart index c155ef60..c68b24e5 100644 --- a/lib/models/messages/filemessage.dart +++ b/lib/models/messages/filemessage.dart @@ -8,7 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../../main.dart'; -import '../../model.dart'; +import '../profile.dart'; class FileMessage extends Message { final MessageMetadata metadata; diff --git a/lib/models/messages/invitemessage.dart b/lib/models/messages/invitemessage.dart index 2965abc9..50a28806 100644 --- a/lib/models/messages/invitemessage.dart +++ b/lib/models/messages/invitemessage.dart @@ -7,7 +7,7 @@ import 'package:cwtch/widgets/messagerow.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import '../../model.dart'; +import '../profile.dart'; class InviteMessage extends Message { final MessageMetadata metadata; diff --git a/lib/models/messages/quotedmessage.dart b/lib/models/messages/quotedmessage.dart index 2eb54b28..7d34c1b5 100644 --- a/lib/models/messages/quotedmessage.dart +++ b/lib/models/messages/quotedmessage.dart @@ -9,7 +9,6 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import '../../main.dart'; -import '../../model.dart'; class QuotedMessageStructure { final String quotedHash; diff --git a/lib/models/messages/textmessage.dart b/lib/models/messages/textmessage.dart index a8d7f6af..44f99510 100644 --- a/lib/models/messages/textmessage.dart +++ b/lib/models/messages/textmessage.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; -import '../../model.dart'; - class TextMessage extends Message { final MessageMetadata metadata; final String content; diff --git a/lib/models/profile.dart b/lib/models/profile.dart new file mode 100644 index 00000000..a4406b69 --- /dev/null +++ b/lib/models/profile.dart @@ -0,0 +1,272 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; + +import 'contact.dart'; +import 'contactlist.dart'; +import 'filedownloadprogress.dart'; +import 'profileservers.dart'; + +class ProfileInfoState extends ChangeNotifier { + ProfileServerListState _servers = ProfileServerListState(); + ContactListState _contacts = ContactListState(); + final String onion; + String _nickname = ""; + String _imagePath = ""; + int _unreadMessages = 0; + bool _online = false; + Map _downloads = Map(); + + // assume profiles are encrypted...this will be set to false + // in the constructor if the profile is encrypted with the defacto password. + bool _encrypted = true; + + ProfileInfoState({ + required this.onion, + nickname = "", + imagePath = "", + unreadMessages = 0, + contactsJson = "", + serversJson = "", + online = false, + encrypted = true, + }) { + this._nickname = nickname; + this._imagePath = imagePath; + this._unreadMessages = unreadMessages; + this._online = online; + this._encrypted = encrypted; + + _contacts.connectServers(this._servers); + + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + this.replaceServers(serversJson); + + List contacts = jsonDecode(contactsJson); + this._contacts.addAll(contacts.map((contact) { + return ContactInfoState(this.onion, contact["identifier"], contact["onion"], + nickname: contact["name"], + status: contact["status"], + imagePath: contact["picture"], + accepted: contact["accepted"], + blocked: contact["blocked"], + savePeerHistory: contact["saveConversationHistory"], + numMessages: contact["numMessages"], + numUnread: contact["numUnread"], + isGroup: contact["isGroup"], + server: contact["groupServer"], + archived: contact["isArchived"] == true, + lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"]))); + })); + + // dummy set to invoke sort-on-load + if (this._contacts.num > 0) { + this._contacts.updateLastMessageTime(this._contacts.contacts.first.identifier, this._contacts.contacts.first.lastMessageTime); + } + } + } + + // Parse out the server list json into our server info state struct... + void replaceServers(String serversJson) { + if (serversJson != "" && serversJson != "null") { + List servers = jsonDecode(serversJson); + this._servers.replace(servers.map((server) { + // TODO Keys... + return RemoteServerInfoState(onion: server["onion"], identifier: server["identifier"], description: server["description"], status: server["status"]); + })); + + this._contacts.contacts.forEach((contact) { + if (contact.isGroup) { + _servers.addGroup(contact); + } + }); + + notifyListeners(); + } + } + + // + void updateServerStatusCache(String server, String status) { + this._servers.updateServerState(server, status); + notifyListeners(); + } + + // Getters and Setters for Online Status + bool get isOnline => this._online; + set isOnline(bool newValue) { + this._online = newValue; + notifyListeners(); + } + + // Check encrypted status for profile info screen + bool get isEncrypted => this._encrypted; + + String get nickname => this._nickname; + set nickname(String newValue) { + this._nickname = newValue; + notifyListeners(); + } + + String get imagePath => this._imagePath; + set imagePath(String newVal) { + this._imagePath = newVal; + notifyListeners(); + } + + int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + this._unreadMessages = newVal; + notifyListeners(); + } + + // Remove a contact from a list. Currently only used when rejecting a group invitation. + // Eventually will also be used for other removals. + void removeContact(String handle) { + this.contactList.removeContactByHandle(handle); + notifyListeners(); + } + + ContactListState get contactList => this._contacts; + ProfileServerListState get serverList => this._servers; + + @override + void dispose() { + super.dispose(); + print("profileinfostate.dispose()"); + } + + void updateFrom(String onion, String name, String picture, String contactsJson, String serverJson, bool online) { + this._nickname = name; + this._imagePath = picture; + this._online = online; + this.replaceServers(serverJson); + + if (contactsJson != null && contactsJson != "" && contactsJson != "null") { + List contacts = jsonDecode(contactsJson); + contacts.forEach((contact) { + var profileContact = this._contacts.getContact(contact["identifier"]); + if (profileContact != null) { + profileContact.status = contact["status"]; + profileContact.totalMessages = contact["numMessages"]; + profileContact.unreadMessages = contact["numUnread"]; + profileContact.lastMessageTime = DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])); + } else { + this._contacts.add(ContactInfoState( + this.onion, + contact["identifier"], + contact["onion"], + nickname: contact["name"], + status: contact["status"], + imagePath: contact["picture"], + accepted: contact["accepted"], + blocked: contact["blocked"], + savePeerHistory: contact["saveConversationHistory"], + numMessages: contact["numMessages"], + numUnread: contact["numUnread"], + isGroup: contact["isGroup"], + server: contact["groupServer"], + lastMessageTime: DateTime.fromMillisecondsSinceEpoch(1000 * int.parse(contact["lastMsgTime"])), + )); + } + }); + } + this._contacts.resort(); + } + + void downloadInit(String fileKey, int numChunks) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + } + + void downloadUpdate(String fileKey, int progress, int numChunks) { + if (!downloadActive(fileKey)) { + this._downloads[fileKey] = FileDownloadProgress(numChunks, DateTime.now()); + if (progress < 0) { + this._downloads[fileKey]!.interrupted = true; + } + } else { + if (this._downloads[fileKey]!.interrupted) { + this._downloads[fileKey]!.interrupted = false; + } + this._downloads[fileKey]!.chunksDownloaded = progress; + this._downloads[fileKey]!.chunksTotal = numChunks; + } + notifyListeners(); + } + + void downloadMarkManifest(String fileKey) { + if (!downloadActive(fileKey)) { + this._downloads[fileKey] = FileDownloadProgress(1, DateTime.now()); + } + this._downloads[fileKey]!.gotManifest = true; + notifyListeners(); + } + + void downloadMarkFinished(String fileKey, String finalPath) { + if (!downloadActive(fileKey)) { + // happens as a result of a CheckDownloadStatus call, + // invoked from a historical (timeline) download message + // so setting numChunks correctly shouldn't matter + this.downloadInit(fileKey, 1); + } + // only update if different + if (!this._downloads[fileKey]!.complete) { + this._downloads[fileKey]!.timeEnd = DateTime.now(); + this._downloads[fileKey]!.downloadedTo = finalPath; + this._downloads[fileKey]!.complete = true; + notifyListeners(); + } + } + + bool downloadKnown(String fileKey) { + return this._downloads.containsKey(fileKey); + } + + bool downloadActive(String fileKey) { + return this._downloads.containsKey(fileKey) && !this._downloads[fileKey]!.interrupted; + } + + bool downloadGotManifest(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.gotManifest; + } + + bool downloadComplete(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.complete; + } + + bool downloadInterrupted(String fileKey) { + return this._downloads.containsKey(fileKey) && this._downloads[fileKey]!.interrupted; + } + + void downloadMarkResumed(String fileKey) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.interrupted = false; + } + } + + double downloadProgress(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.progress() : 0.0; + } + + // used for loading interrupted download info; use downloadMarkFinished for successful downloads + void downloadSetPath(String fileKey, String path) { + if (this._downloads.containsKey(fileKey)) { + this._downloads[fileKey]!.downloadedTo = path; + } + } + + String? downloadFinalPath(String fileKey) { + return this._downloads.containsKey(fileKey) ? this._downloads[fileKey]!.downloadedTo : null; + } + + String downloadSpeed(String fileKey) { + if (!downloadActive(fileKey) || this._downloads[fileKey]!.chunksDownloaded == 0) { + return "0 B/s"; + } + var bytes = this._downloads[fileKey]!.chunksDownloaded * 4096; + var seconds = (this._downloads[fileKey]!.timeEnd ?? DateTime.now()).difference(this._downloads[fileKey]!.timeStart!).inSeconds; + if (seconds == 0) { + return "0 B/s"; + } + return prettyBytes((bytes / seconds).round()) + "/s"; + } +} \ No newline at end of file diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart new file mode 100644 index 00000000..34eb2526 --- /dev/null +++ b/lib/models/profilelist.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; + +import 'profile.dart'; + +class ProfileListState extends ChangeNotifier { + List _profiles = []; + int get num => _profiles.length; + + void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { + var idx = _profiles.indexWhere((element) => element.onion == onion); + if (idx == -1) { + _profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); + } else { + _profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); + } + notifyListeners(); + } + + List get profiles => _profiles.sublist(0); //todo: copy?? dont want caller able to bypass changenotifier + + ProfileInfoState? getProfile(String onion) { + int idx = _profiles.indexWhere((element) => element.onion == onion); + return idx >= 0 ? _profiles[idx] : null; + } + + void delete(String onion) { + _profiles.removeWhere((element) => element.onion == onion); + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/models/profileservers.dart b/lib/models/profileservers.dart index 4b868b95..9dd7af49 100644 --- a/lib/models/profileservers.dart +++ b/lib/models/profileservers.dart @@ -1,6 +1,7 @@ -import 'package:cwtch/model.dart'; import 'package:flutter/material.dart'; +import 'contact.dart'; + class ProfileServerListState extends ChangeNotifier { List _servers = []; diff --git a/lib/views/addcontactview.dart b/lib/views/addcontactview.dart index f8acf20c..2d58c43f 100644 --- a/lib/views/addcontactview.dart +++ b/lib/views/addcontactview.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cwtch/errorHandler.dart'; @@ -13,7 +14,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; /// Add Contact View is the one-stop shop for adding public keys to a Profiles contact list. /// We support both Peers and Groups (experiment-pending). diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 1961ac9e..7d084f5a 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -4,9 +4,9 @@ import 'dart:math'; import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch/cwtch.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:cwtch/widgets/passwordfield.dart'; diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 09cc7d5b..4e7b6ade 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -1,7 +1,10 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/contactlist.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/profileserversview.dart'; import 'package:flutter/material.dart'; -import 'package:cwtch/views/torstatusview.dart'; import 'package:cwtch/widgets/contactrow.dart'; import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/textfield.dart'; @@ -10,7 +13,6 @@ import 'package:provider/provider.dart'; import '../main.dart'; import '../settings.dart'; import 'addcontactview.dart'; -import '../model.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'messageview.dart'; diff --git a/lib/views/doublecolview.dart b/lib/views/doublecolview.dart index 4650c0ed..60714fcf 100644 --- a/lib/views/doublecolview.dart +++ b/lib/views/doublecolview.dart @@ -1,8 +1,10 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import 'contactsview.dart'; import 'messageview.dart'; diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 6407c4ef..18855e7e 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -1,6 +1,8 @@ import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 5d21ab8e..877addb7 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -3,8 +3,12 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/chatmessage.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/messages/quotedmessage.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/profileimage.dart'; @@ -22,7 +26,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:path/path.dart' show basename; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import '../widgets/messagelist.dart'; import 'groupsettingsview.dart'; diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 6606312d..0ab762ee 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -1,8 +1,9 @@ import 'dart:convert'; import 'dart:ui'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:flutter/services.dart'; -import 'package:cwtch/model.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:flutter/material.dart'; diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index 97a7c21b..cbbc3a68 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/profile.dart'; +import 'package:cwtch/models/profilelist.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/settings.dart'; import 'package:cwtch/views/torstatusview.dart'; @@ -13,7 +16,6 @@ import 'package:cwtch/widgets/profilerow.dart'; import 'package:provider/provider.dart'; import '../config.dart'; import '../main.dart'; -import '../model.dart'; import '../torstatus.dart'; import 'addeditprofileview.dart'; import 'globalsettingsview.dart'; diff --git a/lib/views/profileserversview.dart b/lib/views/profileserversview.dart index 1fd029b4..6a4d3ebd 100644 --- a/lib/views/profileserversview.dart +++ b/lib/views/profileserversview.dart @@ -1,3 +1,4 @@ +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/remoteserverrow.dart'; @@ -7,7 +8,6 @@ import 'package:provider/provider.dart'; import '../cwtch_icons_icons.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class ProfileServersView extends StatefulWidget { diff --git a/lib/views/remoteserverview.dart b/lib/views/remoteserverview.dart index 18bcdd04..46ac8567 100644 --- a/lib/views/remoteserverview.dart +++ b/lib/views/remoteserverview.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:cwtch/cwtch/cwtch.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; @@ -17,7 +19,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../errorHandler.dart'; import '../main.dart'; import '../config.dart'; -import '../model.dart'; /// Pane to add or edit a server class RemoteServerView extends StatefulWidget { diff --git a/lib/views/splashView.dart b/lib/views/splashView.dart index 9e68b2f8..fe24af8f 100644 --- a/lib/views/splashView.dart +++ b/lib/views/splashView.dart @@ -1,9 +1,9 @@ +import 'package:cwtch/models/appstate.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../model.dart'; import '../settings.dart'; class SplashView extends StatefulWidget { diff --git a/lib/widgets/DropdownContacts.dart b/lib/widgets/DropdownContacts.dart index aed9c4ac..6c70ecaa 100644 --- a/lib/widgets/DropdownContacts.dart +++ b/lib/widgets/DropdownContacts.dart @@ -1,7 +1,8 @@ +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; bool noFilter(ContactInfoState peer) { return true; diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index ee1ba242..53dfe32c 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/contactsview.dart'; import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; @@ -8,7 +11,6 @@ import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/filebubble.dart b/lib/widgets/filebubble.dart index ea937303..1d6c347c 100644 --- a/lib/widgets/filebubble.dart +++ b/lib/widgets/filebubble.dart @@ -1,14 +1,16 @@ import 'dart:io'; import 'package:cwtch/config.dart'; +import 'package:cwtch/models/contact.dart'; +import 'package:cwtch/models/filedownloadprogress.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:file_picker_desktop/file_picker_desktop.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/invitationbubble.dart b/lib/widgets/invitationbubble.dart index 2839b550..c03cb078 100644 --- a/lib/widgets/invitationbubble.dart +++ b/lib/widgets/invitationbubble.dart @@ -2,12 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../main.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 948351cd..d764d1e6 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -1,13 +1,14 @@ import 'dart:io'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; import 'package:cwtch/third_party/linkify/flutter_linkify.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 57009768..aa29baf2 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -1,11 +1,13 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../model.dart'; import '../settings.dart'; class MessageList extends StatefulWidget { diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 2cc51647..8661305d 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'package:cwtch/cwtch_icons_icons.dart'; +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/views/contactsview.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/profileimage.dart'; @@ -10,7 +13,6 @@ import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class MessageRow extends StatefulWidget { diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 7fccb90f..6d2c8e8e 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -1,3 +1,6 @@ +import 'package:cwtch/models/appstate.dart'; +import 'package:cwtch/models/contactlist.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:cwtch/views/addeditprofileview.dart'; @@ -9,7 +12,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../errorHandler.dart'; import '../main.dart'; -import '../model.dart'; import '../settings.dart'; class ProfileRow extends StatefulWidget { diff --git a/lib/widgets/quotedmessage.dart b/lib/widgets/quotedmessage.dart index e2af135e..31e8a80a 100644 --- a/lib/widgets/quotedmessage.dart +++ b/lib/widgets/quotedmessage.dart @@ -1,9 +1,10 @@ +import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/message.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../model.dart'; import 'package:intl/intl.dart'; import '../settings.dart'; diff --git a/lib/widgets/remoteserverrow.dart b/lib/widgets/remoteserverrow.dart index e7b2417a..c35b0e9a 100644 --- a/lib/widgets/remoteserverrow.dart +++ b/lib/widgets/remoteserverrow.dart @@ -1,4 +1,5 @@ import 'package:cwtch/main.dart'; +import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profileservers.dart'; import 'package:cwtch/models/servers.dart'; import 'package:cwtch/views/addeditservers.dart'; @@ -11,7 +12,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../cwtch_icons_icons.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; class RemoteServerRow extends StatefulWidget { diff --git a/lib/widgets/serverrow.dart b/lib/widgets/serverrow.dart index cb5790df..86f1c821 100644 --- a/lib/widgets/serverrow.dart +++ b/lib/widgets/serverrow.dart @@ -9,7 +9,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../cwtch_icons_icons.dart'; import '../errorHandler.dart'; -import '../model.dart'; import '../settings.dart'; class ServerRow extends StatefulWidget {