forked from cwtch.im/cwtch-ui
Merge pull request 'move all classes in model.dart to their own models/X.dart' (#317) from models into trunk
Reviewed-on: cwtch.im/cwtch-ui#317 Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
This commit is contained in:
commit
47348f3ad7
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
751
lib/model.dart
751
lib/model.dart
|
@ -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<String, dynamic> json)
|
||||
: o = json['o'],
|
||||
d = json['d'];
|
||||
|
||||
Map<String, dynamic> 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<ProfileInfoState> _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<ProfileInfoState> 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<ContactInfoState> _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<ContactInfoState> filteredList() {
|
||||
if (!isFiltered) return contacts;
|
||||
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
|
||||
}
|
||||
|
||||
void addAll(Iterable<ContactInfoState> 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);
|
||||
});
|
||||
//<todo> if(changed) {
|
||||
notifyListeners();
|
||||
//} </todo>
|
||||
}
|
||||
|
||||
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<ContactInfoState> 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<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||
|
||||
// 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<dynamic> 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<dynamic> 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<dynamic> 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<String, GlobalKey<MessageRowState>> keys;
|
||||
late List<MessageCache?> 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, GlobalKey<MessageRowState>>();
|
||||
}
|
||||
|
||||
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<MessageRowState> getMessageKey(int conversation, int message) {
|
||||
String index = "c: " + conversation.toString() + " m:" + message.toString();
|
||||
if (keys[index] == null) {
|
||||
keys[index] = GlobalKey<MessageRowState>();
|
||||
}
|
||||
GlobalKey<MessageRowState> ret = keys[index]!;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
|
||||
String index = "c: " + conversation.toString() + " m:" + message.toString();
|
||||
|
||||
if (keys[index] == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalKey<MessageRowState> 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
class ChatMessage {
|
||||
final int o;
|
||||
final String d;
|
||||
|
||||
ChatMessage({required this.o, required this.d});
|
||||
|
||||
ChatMessage.fromJson(Map<String, dynamic> json)
|
||||
: o = json['o'],
|
||||
d = json['d'];
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'o': o,
|
||||
'd': d,
|
||||
};
|
||||
}
|
|
@ -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<String, GlobalKey<MessageRowState>> keys;
|
||||
late List<MessageCache?> 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, GlobalKey<MessageRowState>>();
|
||||
}
|
||||
|
||||
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<MessageRowState> getMessageKey(int conversation, int message) {
|
||||
String index = "c: " + conversation.toString() + " m:" + message.toString();
|
||||
if (keys[index] == null) {
|
||||
keys[index] = GlobalKey<MessageRowState>();
|
||||
}
|
||||
GlobalKey<MessageRowState> ret = keys[index]!;
|
||||
return ret;
|
||||
}
|
||||
|
||||
GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
|
||||
String index = "c: " + conversation.toString() + " m:" + message.toString();
|
||||
|
||||
if (keys[index] == null) {
|
||||
return null;
|
||||
}
|
||||
GlobalKey<MessageRowState> 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'contact.dart';
|
||||
import 'profileservers.dart';
|
||||
|
||||
class ContactListState extends ChangeNotifier {
|
||||
ProfileServerListState? servers;
|
||||
List<ContactInfoState> _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<ContactInfoState> filteredList() {
|
||||
if (!isFiltered) return contacts;
|
||||
return _contacts.where((ContactInfoState c) => c.onion.toLowerCase().startsWith(_filter) || (c.nickname.toLowerCase().contains(_filter))).toList();
|
||||
}
|
||||
|
||||
void addAll(Iterable<ContactInfoState> 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);
|
||||
});
|
||||
//<todo> if(changed) {
|
||||
notifyListeners();
|
||||
//} </todo>
|
||||
}
|
||||
|
||||
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<ContactInfoState> 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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import 'message.dart';
|
||||
|
||||
class MessageCache {
|
||||
final MessageMetadata metadata;
|
||||
final String wrapper;
|
||||
MessageCache(this.metadata, this.wrapper);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
|
||||
|
||||
// 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<dynamic> 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<dynamic> 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<dynamic> 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";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'profile.dart';
|
||||
|
||||
class ProfileListState extends ChangeNotifier {
|
||||
List<ProfileInfoState> _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<ProfileInfoState> 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();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:cwtch/model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'contact.dart';
|
||||
|
||||
class ProfileServerListState extends ChangeNotifier {
|
||||
List<RemoteServerInfoState> _servers = [];
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue