Compare commits

...

10 Commits

Author SHA1 Message Date
erinn 729ff6811e Merge pull request 'Profile Images' (#355) from custom_profile_images into trunk
continuous-integration/drone/push Build was killed Details
Reviewed-on: #355
Reviewed-by: Dan Ballard <dan@openprivacy.ca>
Reviewed-by: erinn <erinn@openprivacy.ca>
2022-02-07 23:16:46 +00:00
Sarah Jamie Lewis bf4cfde7df Fixup Context Listen
continuous-integration/drone/pr Build is pending Details
2022-02-07 15:16:02 -08:00
Sarah Jamie Lewis 403454d6b8 Add Edit Badge
continuous-integration/drone/pr Build is pending Details
2022-02-07 15:12:36 -08:00
Sarah Jamie Lewis d902ba5cce Rename Constant
continuous-integration/drone/pr Build is pending Details
2022-02-07 14:59:09 -08:00
Sarah Jamie Lewis 5b5fe586e8 Update Lib Cwtch, Allow Deleting P2P contacts, Formatting
continuous-integration/drone/pr Build is passing Details
2022-02-07 14:53:33 -08:00
Sarah Jamie Lewis b280765631 Fallback to Default Profile Images when Image Previews are Disabled 2022-02-07 14:26:14 -08:00
Sarah Jamie Lewis 2a2d808b60 Disable image previews when file sharing is disables
continuous-integration/drone/pr Build is pending Details
2022-02-07 12:23:26 -08:00
Sarah Jamie Lewis d158d7d619 Select Profile Image tooltip + restrict selection only when image previews are enabled 2022-02-07 12:20:54 -08:00
Sarah Jamie Lewis c6192ef736 Factor out showFilePicker into a generic controller
continuous-integration/drone/pr Build is passing Details
2022-02-07 11:30:17 -08:00
Sarah Jamie Lewis 3d85883f8e Profile Images
continuous-integration/drone/pr Build is pending Details
2022-02-04 16:57:31 -08:00
28 changed files with 266 additions and 109 deletions

View File

@ -1 +1 @@
2022-02-04-16-57-v1.5.4-28-g4e4e331 2022-02-07-17-39-v1.5.4-31-g17acc3b

View File

@ -1 +1 @@
2022-02-04-21-58-v1.5.4-28-g4e4e331 2022-02-07-22-31-v1.5.4-31-g17acc3b

View File

@ -77,7 +77,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
} }
val loader = FlutterInjector.instance().flutterLoader() val loader = FlutterInjector.instance().flutterLoader()
val key = loader.getLookupKeyForAsset("assets/" + data.getString("Picture"))//"assets/profiles/001-centaur.png") val key = loader.getLookupKeyForAsset("assets/" + data.getString("picture"))//"assets/profiles/001-centaur.png")
val fh = applicationContext.assets.open(key) val fh = applicationContext.assets.open(key)

3
lib/constants.dart Normal file
View File

@ -0,0 +1,3 @@
const int MaxImageFileSharingSize = 20971520;
const int MaxGeneralFileSharingSize = 10737418240;

View File

@ -0,0 +1,30 @@
import 'dart:io';
import 'package:cwtch/models/appstate.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
void showFilePicker(BuildContext ctx, int maxBytes, Function(File) onSuccess, Function onError, Function onCancel) async {
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
FilePickerResult? result = await FilePicker.platform.pickFiles(lockParentWindow: true);
appstate.disableFilePicker = false;
if (result != null && result.files.first.path != null) {
File file = File(result.files.first.path!);
// We have a maximum number of bytes we can represent in terms of
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
if (file.lengthSync() <= maxBytes) {
onSuccess(file);
} else {
onError();
}
} else {
onCancel();
}
}

View File

@ -55,7 +55,7 @@ class CwtchNotifier {
} }
EnvironmentConfig.debugLog("NewPeer $data"); EnvironmentConfig.debugLog("NewPeer $data");
// if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta... // if tag != v1-defaultPassword then it is either encrypted OR it is an unencrypted account created during pre-beta...
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword"); profileCN.add(data["Identity"], data["name"], data["picture"], data["defaultPicture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true", data["tag"] != "v1-defaultPassword");
break; break;
case "ContactCreated": case "ContactCreated":
EnvironmentConfig.debugLog("ContactCreated $data"); EnvironmentConfig.debugLog("ContactCreated $data");
@ -67,6 +67,7 @@ class CwtchNotifier {
nickname: data["nick"], nickname: data["nick"],
status: data["status"], status: data["status"],
imagePath: data["picture"], imagePath: data["picture"],
defaultImagePath: data["defaultPicture"],
blocked: data["blocked"] == "true", blocked: data["blocked"] == "true",
accepted: data["accepted"] == "true", accepted: data["accepted"] == "true",
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"], savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
@ -106,7 +107,8 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], int.parse(data["ConversationID"]), data["GroupID"],
blocked: false, // we created blocked: false, // we created
accepted: true, // we created accepted: true, // we created
imagePath: data["PicturePath"], imagePath: data["picture"],
defaultImagePath: data["picture"],
nickname: data["GroupName"], nickname: data["GroupName"],
status: status, status: status,
server: data["GroupServer"], server: data["GroupServer"],
@ -147,7 +149,7 @@ class CwtchNotifier {
var messageID = int.parse(data["Index"]); var messageID = int.parse(data["Index"]);
var timestamp = DateTime.tryParse(data['TimestampReceived'])!; var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
var senderHandle = data['RemotePeer']; var senderHandle = data['RemotePeer'];
var senderImage = data['Picture']; var senderImage = data['picture'];
var isAuto = data['Auto'] == "true"; var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash']; String? contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"]; var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
@ -199,7 +201,7 @@ class CwtchNotifier {
if (data["ProfileOnion"] != data["RemotePeer"]) { if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]); var idx = int.parse(data["Index"]);
var senderHandle = data['RemotePeer']; var senderHandle = data['RemotePeer'];
var senderImage = data['Picture']; var senderImage = data['picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!; var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier); var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var currentTotal = contact!.totalMessages; var currentTotal = contact!.totalMessages;
@ -301,7 +303,7 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
blocked: false, // NewGroup only issued on accepting invite blocked: false, // NewGroup only issued on accepting invite
accepted: true, // NewGroup only issued on accepting invite accepted: true, // NewGroup only issued on accepting invite
imagePath: data["PicturePath"], imagePath: data["picture"],
nickname: groupInvite["GroupName"], nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"], server: groupInvite["ServerHost"],
status: status, status: status,
@ -322,15 +324,28 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort(); profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
break; break;
case "NewRetValMessageFromPeer": case "NewRetValMessageFromPeer":
if (data["Path"] == "profile.name") { if (data["Path"] == "profile.name" && data["Exists"] == "true") {
if (data["Data"].toString().trim().length > 0) { if (data["Data"].toString().trim().length > 0) {
// Update locally on the UI... // Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"]; profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
} }
} }
} else if (data['Path'] == "profile.picture") { } else if (data['Path'] == "profile.custom-profile-image" && data["Exists"] == "true") {
// Not yet.. EnvironmentConfig.debugLog("received ret val of custom profile image: $data");
String fileKey = data['Data'];
String filePath = data['FilePath'];
bool downloaded = data['FileDownloadFinished'] == "true";
if (downloaded) {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.imagePath = filePath;
}
} else {
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]);
if (contact != null) {
profileCN.getProfile(data["ProfileOnion"])?.waitForDownloadComplete(contact.identifier, fileKey);
}
}
} else { } else {
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}"); EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
} }

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -162,7 +163,7 @@
"newPassword": "Neues Passwort", "newPassword": "Neues Passwort",
"yesLeave": "Ja, diese Unterhaltung beenden", "yesLeave": "Ja, diese Unterhaltung beenden",
"reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.", "reallyLeaveThisGroupPrompt": "Bist du sicher, dass du diese Unterhaltung beenden möchtest? Alle Nachrichten und Attribute werden gelöscht.",
"leaveGroup": "Unterhaltung beenden", "leaveConversation": "Unterhaltung beenden",
"inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:", "inviteToGroup": "Du wurdest eingeladen einer Gruppe beizutreten:",
"titleManageServers": "Server verwalten", "titleManageServers": "Server verwalten",
"dateNever": "Nie", "dateNever": "Nie",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"editProfile": "Edit Profile", "editProfile": "Edit Profile",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
@ -164,7 +165,7 @@
"newPassword": "New Password", "newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation", "yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation", "leaveConversation": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation", "pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"tooltipAddContact": "Add a new contact or conversation", "tooltipAddContact": "Add a new contact or conversation",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -148,7 +149,7 @@
"newPassword": "New Password", "newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation", "yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation", "leaveConversation": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"titleManageServers": "Manage Servers", "titleManageServers": "Manage Servers",
"dateNever": "Never", "dateNever": "Never",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"editProfile": "Modifier le profil", "editProfile": "Modifier le profil",
"settingTheme": "Utilisez des thèmes clairs", "settingTheme": "Utilisez des thèmes clairs",
"torSettingsUseCustomTorServiceConfiguration": "Utiliser une configuration personnalisée du service Tor (torrc)", "torSettingsUseCustomTorServiceConfiguration": "Utiliser une configuration personnalisée du service Tor (torrc)",
@ -213,7 +214,7 @@
"dateNever": "Jamais", "dateNever": "Jamais",
"titleManageServers": "Gérer les serveurs", "titleManageServers": "Gérer les serveurs",
"inviteToGroup": "Vous avez été invité à rejoindre un groupe :", "inviteToGroup": "Vous avez été invité à rejoindre un groupe :",
"leaveGroup": "Quittez cette conversation", "leaveConversation": "Quittez cette conversation",
"reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.", "reallyLeaveThisGroupPrompt": "Êtes-vous sûr de vouloir quitter cette conversation ? Tous les messages et attributs seront supprimés.",
"yesLeave": "Oui, quittez cette conversation", "yesLeave": "Oui, quittez cette conversation",
"noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.", "noPasswordWarning": "Ne pas utiliser de mot de passe sur ce compte signifie que toutes les données stockées localement ne seront pas chiffrées.",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -38,7 +39,7 @@
"copiedToClipboardNotification": "Copiato negli Appunti", "copiedToClipboardNotification": "Copiato negli Appunti",
"groupNameLabel": "Nome del gruppo", "groupNameLabel": "Nome del gruppo",
"titleManageServers": "Gestisci i Server", "titleManageServers": "Gestisci i Server",
"leaveGroup": "Lascia Questa Conversazione", "leaveConversation": "Lascia Questa Conversazione",
"yesLeave": "Sì, Lascia Questa Conversazione", "yesLeave": "Sì, Lascia Questa Conversazione",
"newPassword": "Nuova Password", "newPassword": "Nuova Password",
"sendMessage": "Invia Messaggio", "sendMessage": "Invia Messaggio",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "pl", "@@locale": "pl",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -209,7 +210,7 @@
"chatHistoryDefault": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.", "chatHistoryDefault": "Ta konwersacja zostanie usunięta gdy zamkniesz Cwtch! Możesz włączyć zapisywanie wiadomości dla każdej konwersacji osobno w menu w prawym górnym rogu.",
"yesLeave": "Opuść", "yesLeave": "Opuść",
"reallyLeaveThisGroupPrompt": "Na pewno chcesz opuścić tę grupę? Wszystkie wiadomości i atrybuty zostaną usunięte.", "reallyLeaveThisGroupPrompt": "Na pewno chcesz opuścić tę grupę? Wszystkie wiadomości i atrybuty zostaną usunięte.",
"leaveGroup": "Opuść grupę", "leaveConversation": "Opuść grupę",
"inviteToGroup": "Zaproszono Cię do grupy:", "inviteToGroup": "Zaproszono Cię do grupy:",
"dateNever": "Nigdy", "dateNever": "Nigdy",
"dateLastYear": "Rok temu", "dateLastYear": "Rok temu",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -148,7 +149,7 @@
"newPassword": "New Password", "newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation", "yesLeave": "Yes, Leave This Conversation",
"reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.", "reallyLeaveThisGroupPrompt": "Are you sure you want to leave this conversation? All messages and attributes will be deleted.",
"leaveGroup": "Leave This Conversation", "leaveConversation": "Leave This Conversation",
"inviteToGroup": "You have been invited to join a group:", "inviteToGroup": "You have been invited to join a group:",
"titleManageServers": "Manage Servers", "titleManageServers": "Manage Servers",
"dateNever": "Never", "dateNever": "Never",

View File

@ -1,6 +1,7 @@
{ {
"@@locale": "ru", "@@locale": "ru",
"@@last_modified": "2022-01-28T19:57:41+01:00", "@@last_modified": "2022-02-07T21:17:01+01:00",
"tooltipSelectACustomProfileImage": "Select a Custom Profile Image",
"torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.", "torSettingsEnabledCacheDescription": "Cache the current downloaded Tor consensus to reuse next time Cwtch is opened. This will allow Tor to start faster. When disabled, Cwtch will purge cached data on start up.",
"torSettingsEnableCache": "Cache Tor Consensus", "torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network", "labelTorNetwork": "Tor Network",
@ -164,7 +165,7 @@
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"yesLeave": "Да, оставить этот чат", "yesLeave": "Да, оставить этот чат",
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.", "reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveGroup": "Да, оставить этот чат", "leaveConversation": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:", "inviteToGroup": "Вас пригласили присоединиться к группе:",
"titleManageServers": "Управление серверами", "titleManageServers": "Управление серверами",
"dateNever": "Никогда", "dateNever": "Никогда",

View File

@ -14,6 +14,7 @@ class ContactInfoState extends ChangeNotifier {
late bool _blocked; late bool _blocked;
late String _status; late String _status;
late String _imagePath; late String _imagePath;
late String _defaultImagePath;
late String _savePeerHistory; late String _savePeerHistory;
late int _unreadMessages = 0; late int _unreadMessages = 0;
late int _totalMessages = 0; late int _totalMessages = 0;
@ -37,6 +38,7 @@ class ContactInfoState extends ChangeNotifier {
blocked = false, blocked = false,
status = "", status = "",
imagePath = "", imagePath = "",
defaultImagePath = "",
savePeerHistory = "DeleteHistoryConfirmed", savePeerHistory = "DeleteHistoryConfirmed",
numMessages = 0, numMessages = 0,
numUnread = 0, numUnread = 0,
@ -49,6 +51,7 @@ class ContactInfoState extends ChangeNotifier {
this._blocked = blocked; this._blocked = blocked;
this._status = status; this._status = status;
this._imagePath = imagePath; this._imagePath = imagePath;
this._defaultImagePath = defaultImagePath;
this._totalMessages = numMessages; this._totalMessages = numMessages;
this._unreadMessages = numUnread; this._unreadMessages = numUnread;
this._savePeerHistory = savePeerHistory; this._savePeerHistory = savePeerHistory;
@ -166,6 +169,13 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String get defaultImagePath => this._defaultImagePath;
set defaultImagePath(String newVal) {
this._defaultImagePath = newVal;
notifyListeners();
}
DateTime get lastMessageTime => this._lastMessageTime; DateTime get lastMessageTime => this._lastMessageTime;
set lastMessageTime(DateTime newVal) { set lastMessageTime(DateTime newVal) {

View File

@ -14,9 +14,11 @@ class ProfileInfoState extends ChangeNotifier {
final String onion; final String onion;
String _nickname = ""; String _nickname = "";
String _imagePath = ""; String _imagePath = "";
String _defaultImagePath = "";
int _unreadMessages = 0; int _unreadMessages = 0;
bool _online = false; bool _online = false;
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>(); Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
Map<String, int> _downloadTriggers = Map<String, int>();
// assume profiles are encrypted...this will be set to false // assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password. // in the constructor if the profile is encrypted with the defacto password.
@ -26,14 +28,17 @@ class ProfileInfoState extends ChangeNotifier {
required this.onion, required this.onion,
nickname = "", nickname = "",
imagePath = "", imagePath = "",
defaultImagePath = "",
unreadMessages = 0, unreadMessages = 0,
contactsJson = "", contactsJson = "",
serversJson = "", serversJson = "",
online = false, online = false,
encrypted = true, encrypted = true,
String,
}) { }) {
this._nickname = nickname; this._nickname = nickname;
this._imagePath = imagePath; this._imagePath = imagePath;
this._defaultImagePath = defaultImagePath;
this._unreadMessages = unreadMessages; this._unreadMessages = unreadMessages;
this._online = online; this._online = online;
this._encrypted = encrypted; this._encrypted = encrypted;
@ -49,6 +54,7 @@ class ProfileInfoState extends ChangeNotifier {
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
defaultImagePath: contact["isGroup"] ? contact["picture"] : contact["defaultPicture"],
accepted: contact["accepted"], accepted: contact["accepted"],
blocked: contact["blocked"], blocked: contact["blocked"],
savePeerHistory: contact["saveConversationHistory"], savePeerHistory: contact["saveConversationHistory"],
@ -114,6 +120,12 @@ class ProfileInfoState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String get defaultImagePath => this._defaultImagePath;
set defaultImagePath(String newVal) {
this._defaultImagePath = newVal;
notifyListeners();
}
int get unreadMessages => this._unreadMessages; int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) { set unreadMessages(int newVal) {
this._unreadMessages = newVal; this._unreadMessages = newVal;
@ -160,6 +172,7 @@ class ProfileInfoState extends ChangeNotifier {
contact["identifier"], contact["identifier"],
contact["onion"], contact["onion"],
nickname: contact["name"], nickname: contact["name"],
defaultImagePath: contact["defaultPicture"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
accepted: contact["accepted"], accepted: contact["accepted"],
@ -178,22 +191,14 @@ class ProfileInfoState extends ChangeNotifier {
this._contacts.resort(); this._contacts.resort();
} }
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) { void newMessage(
int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) {
if (!selectedProfile) { if (!selectedProfile) {
unreadMessages++; unreadMessages++;
notifyListeners(); notifyListeners();
} }
contactList.newMessage( contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
identifier,
messageID,
timestamp,
senderHandle,
senderImage,
isAuto,
data,
contenthash,
selectedConversation);
} }
void downloadInit(String fileKey, int numChunks) { void downloadInit(String fileKey, int numChunks) {
@ -232,6 +237,15 @@ class ProfileInfoState extends ChangeNotifier {
// so setting numChunks correctly shouldn't matter // so setting numChunks correctly shouldn't matter
this.downloadInit(fileKey, 1); this.downloadInit(fileKey, 1);
} }
// Update the contact with a custom profile image if we are
// waiting for one...
if (this._downloadTriggers.containsKey(fileKey)) {
int identifier = this._downloadTriggers[fileKey]!;
this.contactList.getContact(identifier)!.imagePath = finalPath;
notifyListeners();
}
// only update if different // only update if different
if (!this._downloads[fileKey]!.complete) { if (!this._downloads[fileKey]!.complete) {
this._downloads[fileKey]!.timeEnd = DateTime.now(); this._downloads[fileKey]!.timeEnd = DateTime.now();
@ -308,4 +322,9 @@ class ProfileInfoState extends ChangeNotifier {
} }
return prettyBytes((bytes / seconds).round()) + "/s"; return prettyBytes((bytes / seconds).round()) + "/s";
} }
void waitForDownloadComplete(int identifier, String fileKey) {
_downloadTriggers[fileKey] = identifier;
notifyListeners();
}
} }

View File

@ -6,10 +6,11 @@ class ProfileListState extends ChangeNotifier {
List<ProfileInfoState> _profiles = []; List<ProfileInfoState> _profiles = [];
int get num => _profiles.length; int get num => _profiles.length;
void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { void add(String onion, String name, String picture, String defaultPicture, String contactsJson, String serverJson, bool online, bool encrypted) {
var idx = _profiles.indexWhere((element) => element.onion == onion); var idx = _profiles.indexWhere((element) => element.onion == onion);
if (idx == -1) { if (idx == -1) {
_profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted)); _profiles.add(ProfileInfoState(
onion: onion, nickname: name, imagePath: picture, defaultImagePath: defaultPicture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted));
} else { } else {
_profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online); _profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online);
} }
@ -28,7 +29,5 @@ class ProfileListState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
int generateUnreadCount(String selectedProfile) => _profiles.where( (p) => p.onion != selectedProfile ).fold(0, (i, p) => i + p.unreadMessages); int generateUnreadCount(String selectedProfile) => _profiles.where((p) => p.onion != selectedProfile).fold(0, (i, p) => i + p.unreadMessages);
} }

View File

@ -1,10 +1,10 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:cwtch/config.dart'; import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch/cwtch.dart'; import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profile.dart';
import 'package:cwtch/controllers/filesharing.dart' as filesharing;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/buttontextfield.dart';
@ -15,6 +15,7 @@ import 'package:cwtch/widgets/textfield.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../constants.dart';
import '../cwtch_icons_icons.dart'; import '../cwtch_icons_icons.dart';
import '../errorHandler.dart'; import '../errorHandler.dart';
import '../main.dart'; import '../main.dart';
@ -89,14 +90,39 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Visibility( Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty, visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
ProfileImage( MouseRegion(
imagePath: Provider.of<ProfileInfoState>(context).imagePath, cursor: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic,
diameter: 120, child: GestureDetector(
maskOut: false, // don't allow setting of profile images if the image previews experiment is disabled.
border: theme.theme.portraitOnlineBorderColor, onTap: Provider.of<AppState>(context, listen: false).disableFilePicker || !Provider.of<Settings>(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment)
badgeTextColor: Colors.red, ? null
badgeColor: Colors.red, : () {
) filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) {
var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
// Share this image publicly (conversation handle == -1)
Provider.of<FlwtchState>(context, listen: false).cwtch.ShareFile(profile, -1, file.path);
// update the image cache locally
Provider.of<ProfileInfoState>(context, listen: false).imagePath = file.path;
}, () {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.msgFileTooBig),
duration: Duration(seconds: 4),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, () {});
},
child: ProfileImage(
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
? Provider.of<ProfileInfoState>(context).imagePath
: Provider.of<ProfileInfoState>(context).defaultImagePath,
diameter: 120,
tooltip:
Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "",
maskOut: false,
border: theme.theme.portraitOnlineBorderColor,
badgeTextColor: theme.theme.portraitContactBadgeTextColor,
badgeColor: theme.theme.portraitContactBadgeColor,
badgeEdit: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment))))
])), ])),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),

View File

@ -86,11 +86,10 @@ class _ContactsViewState extends State<ContactsView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
StreamBuilder<bool>( StreamBuilder<bool>(
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(), stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) { builder: (BuildContext context, AsyncSnapshot<bool> unreadCountSnapshot) {
int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "") ; int unreadCount = Provider.of<ProfileListState>(context).generateUnreadCount(Provider.of<AppState>(context).selectedProfile ?? "");
return Visibility( return Visibility(
visible: unreadCount > 0, visible: unreadCount > 0,
@ -104,7 +103,9 @@ class _ContactsViewState extends State<ContactsView> {
title: RepaintBoundary( title: RepaintBoundary(
child: Row(children: [ child: Row(children: [
ProfileImage( ProfileImage(
imagePath: Provider.of<ProfileInfoState>(context).imagePath, imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
? Provider.of<ProfileInfoState>(context).imagePath
: Provider.of<ProfileInfoState>(context).defaultImagePath,
diameter: 42, diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor, border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
badgeTextColor: Colors.red, badgeTextColor: Colors.red,

View File

@ -251,6 +251,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
settings.enableExperiment(FileSharingExperiment); settings.enableExperiment(FileSharingExperiment);
} else { } else {
settings.disableExperiment(FileSharingExperiment); settings.disableExperiment(FileSharingExperiment);
settings.disableExperiment(ImagePreviewsExperiment);
} }
saveSettings(context); saveSettings(context);
}, },

View File

@ -159,7 +159,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
), ),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip( Tooltip(
message: AppLocalizations.of(context)!.leaveGroup, message: AppLocalizations.of(context)!.leaveConversation,
child: TextButton.icon( child: TextButton.icon(
onPressed: () { onPressed: () {
showAlertDialog(context); showAlertDialog(context);
@ -167,7 +167,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)), style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
icon: Icon(CwtchIcons.leave_group), icon: Icon(CwtchIcons.leave_group),
label: Text( label: Text(
AppLocalizations.of(context)!.leaveGroup, AppLocalizations.of(context)!.leaveConversation,
style: TextStyle(decoration: TextDecoration.underline), style: TextStyle(decoration: TextDecoration.underline),
), ),
)) ))

View File

@ -11,7 +11,7 @@ import 'package:cwtch/models/profile.dart';
import 'package:cwtch/widgets/malformedbubble.dart'; import 'package:cwtch/widgets/malformedbubble.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart'; import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:cwtch/controllers/filesharing.dart' as filesharing;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -23,6 +23,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../constants.dart';
import '../main.dart'; import '../main.dart';
import '../settings.dart'; import '../settings.dart';
import '../widgets/messagelist.dart'; import '../widgets/messagelist.dart';
@ -92,7 +93,16 @@ class _MessageViewState extends State<MessageView> {
onPressed: Provider.of<AppState>(context).disableFilePicker onPressed: Provider.of<AppState>(context).disableFilePicker
? null ? null
: () { : () {
_showFilePicker(context); imagePreview = null;
filesharing.showFilePicker(context, MaxGeneralFileSharingSize, (File file) {
_confirmFileSend(context, file.path);
}, () {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.msgFileTooBig),
duration: Duration(seconds: 4),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}, () {});
}, },
)); ));
} }
@ -129,7 +139,9 @@ class _MessageViewState extends State<MessageView> {
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null, leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
title: Row(children: [ title: Row(children: [
ProfileImage( ProfileImage(
imagePath: Provider.of<ContactInfoState>(context).imagePath, imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment)
? Provider.of<ContactInfoState>(context).imagePath
: Provider.of<ContactInfoState>(context).defaultImagePath,
diameter: 42, diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor, border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
badgeTextColor: Colors.red, badgeTextColor: Colors.red,
@ -415,36 +427,6 @@ class _MessageViewState extends State<MessageView> {
}); });
} }
void _showFilePicker(BuildContext ctx) async {
imagePreview = null;
// only allow one file picker at a time
// note: ideally we would destroy file picker when leaving a conversation
// but we don't currently have that option.
// we need to store AppState in a variable because ctx might be destroyed
// while awaiting for pickFiles.
var appstate = Provider.of<AppState>(ctx, listen: false);
appstate.disableFilePicker = true;
// currently lockParentWindow only works on Windows...
FilePickerResult? result = await FilePicker.platform.pickFiles(lockParentWindow: true);
appstate.disableFilePicker = false;
if (result != null && result.files.first.path != null) {
File file = File(result.files.first.path!);
// We have a maximum number of bytes we can represent in terms of
// a manifest (see : https://git.openprivacy.ca/cwtch.im/cwtch/src/branch/master/protocol/files/manifest.go#L25)
if (file.lengthSync() <= 10737418240) {
print("Sending " + file.path);
_confirmFileSend(ctx, file.path);
} else {
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.msgFileTooBig),
duration: Duration(seconds: 4),
);
ScaffoldMessenger.of(ctx).showSnackBar(snackBar);
}
}
}
void _confirmFileSend(BuildContext ctx, String path) async { void _confirmFileSend(BuildContext ctx, String path) async {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: ctx, context: ctx,

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/profile.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:cwtch/widgets/buttontextfield.dart'; import 'package:cwtch/widgets/buttontextfield.dart';
import 'package:cwtch/widgets/cwtchlabel.dart'; import 'package:cwtch/widgets/cwtchlabel.dart';
@ -216,11 +217,29 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog
}); });
}, },
icon: Icon(CwtchIcons.leave_chat), icon: Icon(Icons.archive),
label: Text(AppLocalizations.of(context)!.archiveConversation), label: Text(AppLocalizations.of(context)!.archiveConversation),
)) ))
]),
SizedBox(
height: 20,
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveConversation,
child: TextButton.icon(
onPressed: () {
showAlertDialog(context);
},
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
icon: Icon(CwtchIcons.leave_group),
label: Text(
AppLocalizations.of(context)!.leaveConversation,
style: TextStyle(decoration: TextDecoration.underline),
),
))
]) ])
]), ])
]))))); ])))));
}); });
}); });
@ -246,10 +265,10 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
child: Text(AppLocalizations.of(context)!.yesLeave), child: Text(AppLocalizations.of(context)!.yesLeave),
onPressed: () { onPressed: () {
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var handle = Provider.of<ContactInfoState>(context, listen: false).identifier; var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
// locally update cache... // locally update cache...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true; Provider.of<ProfileInfoState>(context, listen: false).contactList.removeContact(identifier);
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle); Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, identifier);
Future.delayed(Duration(milliseconds: 500), () { Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null; Provider.of<AppState>(context, listen: false).selectedConversation = null;
Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog

View File

@ -50,7 +50,7 @@ class _ContactRowState extends State<ContactRow> {
badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor, badgeColor: Provider.of<Settings>(context).theme.portraitContactBadgeColor,
badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor, badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
diameter: 64.0, diameter: 64.0,
imagePath: contact.imagePath, imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? contact.imagePath : contact.defaultImagePath,
maskOut: !contact.isOnline(), maskOut: !contact.isOnline(),
border: contact.isOnline() border: contact.isOnline()
? Provider.of<Settings>(context).theme.portraitOnlineBorderColor ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor

View File

@ -152,6 +152,12 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
]; ];
} else { } else {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
ContactInfoState? sender = Provider.of<ProfileInfoState>(context).contactList.findContact(Provider.of<MessageMetadata>(context).senderHandle);
String imagePath = Provider.of<MessageMetadata>(context).senderImage!;
if (sender != null) {
imagePath = Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath;
}
Widget wdgPortrait = GestureDetector( Widget wdgPortrait = GestureDetector(
onTap: !isGroup onTap: !isGroup
? null ? null
@ -162,7 +168,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: ProfileImage( child: ProfileImage(
diameter: 48.0, diameter: 48.0,
imagePath: Provider.of<MessageMetadata>(context).senderImage ?? contact.imagePath, // default to the contact image...otherwise use a derived sender image...
imagePath: imagePath,
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor, border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor,
badgeTextColor: Colors.red, badgeTextColor: Colors.red,
badgeColor: Colors.red, badgeColor: Colors.red,

View File

@ -1,3 +1,6 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/themes/opaque.dart'; import 'package:cwtch/themes/opaque.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -6,7 +9,15 @@ import '../settings.dart';
class ProfileImage extends StatefulWidget { class ProfileImage extends StatefulWidget {
ProfileImage( ProfileImage(
{required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false, this.tooltip = ""}); {required this.imagePath,
required this.diameter,
required this.border,
this.badgeCount = 0,
required this.badgeColor,
required this.badgeTextColor,
this.maskOut = false,
this.tooltip = "",
this.badgeEdit = false});
final double diameter; final double diameter;
final String imagePath; final String imagePath;
final Color border; final Color border;
@ -14,6 +25,7 @@ class ProfileImage extends StatefulWidget {
final Color badgeColor; final Color badgeColor;
final Color badgeTextColor; final Color badgeTextColor;
final bool maskOut; final bool maskOut;
final bool badgeEdit;
final String tooltip; final String tooltip;
@override @override
@ -23,8 +35,11 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> { class _ProfileImageState extends State<ProfileImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var image = Image( var file = new File(widget.imagePath);
image: AssetImage("assets/" + widget.imagePath), var image = Image.file(
file,
cacheWidth: 512,
cacheHeight: 512,
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute // We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut colorBlendMode: !widget.maskOut
@ -36,6 +51,21 @@ class _ProfileImageState extends State<ProfileImage> {
isAntiAlias: true, isAntiAlias: true,
width: widget.diameter, width: widget.diameter,
height: widget.diameter, height: widget.diameter,
errorBuilder: (context, error, stackTrace) {
// on android the above will fail for asset images, in which case try to load them the original way
return Image.asset(widget.imagePath,
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut
? Provider.of<Settings>(context).theme.mode == mode_dark
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,
color: Provider.of<Settings>(context).theme.portraitBackgroundColor,
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter);
},
); );
return RepaintBoundary( return RepaintBoundary(
@ -50,14 +80,19 @@ class _ProfileImageState extends State<ProfileImage> {
padding: const EdgeInsets.all(2.0), //border size padding: const EdgeInsets.all(2.0), //border size
child: ClipOval(clipBehavior: Clip.antiAlias, child: widget.tooltip == "" ? image : Tooltip(message: widget.tooltip, child: image))))), child: ClipOval(clipBehavior: Clip.antiAlias, child: widget.tooltip == "" ? image : Tooltip(message: widget.tooltip, child: image))))),
Visibility( Visibility(
visible: widget.badgeCount > 0, visible: widget.badgeEdit || widget.badgeCount > 0,
child: Positioned( child: Positioned(
bottom: 0.0, bottom: 0.0,
right: 0.0, right: 0.0,
child: CircleAvatar( child: CircleAvatar(
radius: 10.0, radius: max(10.0, widget.diameter / 6.0),
backgroundColor: widget.badgeColor, backgroundColor: widget.badgeColor,
child: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)), child: widget.badgeEdit
? Icon(
Icons.edit,
color: widget.badgeTextColor,
)
: Text(widget.badgeCount > 99 ? "99+" : widget.badgeCount.toString(), style: TextStyle(color: widget.badgeTextColor, fontSize: 8.0)),
), ),
)), )),
])); ]));

View File

@ -38,7 +38,7 @@ class _ProfileRowState extends State<ProfileRow> {
badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor, badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor,
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor, badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor,
diameter: 64.0, diameter: 64.0,
imagePath: profile.imagePath, imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? profile.imagePath : profile.defaultImagePath,
border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor)), border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor)),
Expanded( Expanded(
child: Column( child: Column(
@ -105,11 +105,12 @@ class _ProfileRowState extends State<ProfileRow> {
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) { void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Provider.of<ErrorHandler>(context, listen: false).reset(); Provider.of<ErrorHandler>(context, listen: false).reset();
Navigator.of(context).push(MaterialPageRoute<void>( Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext bcontext) {
var profile = Provider.of<FlwtchState>(bcontext).profs.getProfile(onion)!;
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider<ProfileInfoState>( ChangeNotifierProvider<ProfileInfoState>.value(
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted), value: profile,
), ),
], ],
builder: (context, widget) => AddEditProfileView(), builder: (context, widget) => AddEditProfileView(),

View File

@ -8,7 +8,8 @@ doNothing(String x) {}
// Provides a styled Text Field for use in Form Widgets. // Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator. // Callers must provide a text controller, label helper text and a validator.
class CwtchTextField extends StatefulWidget { class CwtchTextField extends StatefulWidget {
CwtchTextField({required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key, this.testKey}); CwtchTextField(
{required this.controller, this.hintText = "", this.validator, this.autofocus = false, this.onChanged = doNothing, this.number = false, this.multiLine = false, this.key, this.testKey});
final TextEditingController controller; final TextEditingController controller;
final String hintText; final String hintText;
final FormFieldValidator? validator; final FormFieldValidator? validator;