Profile Images #355

Merged
erinn merged 9 commits from custom_profile_images into trunk 2022-02-07 23:16:47 +00: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 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)

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");
// 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;
case "ContactCreated":
EnvironmentConfig.debugLog("ContactCreated $data");
@ -67,6 +67,7 @@ class CwtchNotifier {
nickname: data["nick"],
status: data["status"],
imagePath: data["picture"],
defaultImagePath: data["defaultPicture"],
blocked: data["blocked"] == "true",
accepted: data["accepted"] == "true",
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"],
blocked: false, // we created
accepted: true, // we created
imagePath: data["PicturePath"],
imagePath: data["picture"],
defaultImagePath: data["picture"],
nickname: data["GroupName"],
status: status,
server: data["GroupServer"],
@ -147,7 +149,7 @@ class CwtchNotifier {
var messageID = int.parse(data["Index"]);
var timestamp = DateTime.tryParse(data['TimestampReceived'])!;
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var senderImage = data['picture'];
var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
@ -199,7 +201,7 @@ class CwtchNotifier {
if (data["ProfileOnion"] != data["RemotePeer"]) {
var idx = int.parse(data["Index"]);
var senderHandle = data['RemotePeer'];
var senderImage = data['Picture'];
var senderImage = data['picture'];
var timestampSent = DateTime.tryParse(data['TimestampSent'])!;
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var currentTotal = contact!.totalMessages;
@ -301,7 +303,7 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], identifier, groupInvite["GroupID"],
blocked: false, // NewGroup only issued on accepting invite
accepted: true, // NewGroup only issued on accepting invite
imagePath: data["PicturePath"],
imagePath: data["picture"],
nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"],
status: status,
@ -322,15 +324,28 @@ class CwtchNotifier {
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
break;
case "NewRetValMessageFromPeer":
if (data["Path"] == "profile.name") {
if (data["Path"] == "profile.name" && data["Exists"] == "true") {
if (data["Data"].toString().trim().length > 0) {
// Update locally on the UI...
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"]) != null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(data["RemotePeer"])!.nickname = data["Data"];
}
}
} else if (data['Path'] == "profile.picture") {
// Not yet..
} else if (data['Path'] == "profile.custom-profile-image" && data["Exists"] == "true") {
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 {
EnvironmentConfig.debugLog("unhandled ret val event: ${data['Path']}");
}

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
@ -162,7 +163,7 @@
"newPassword": "Neues Passwort",
"yesLeave": "Ja, diese Unterhaltung beenden",
"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:",
"titleManageServers": "Server verwalten",
"dateNever": "Nie",

View File

@ -1,6 +1,7 @@
{
"@@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",
"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",
@ -164,7 +165,7 @@
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"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:",
"pasteAddressToAddContact": "Paste a cwtch address, invitation or key bundle here to add a new conversation",
"tooltipAddContact": "Add a new contact or conversation",

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
@ -148,7 +149,7 @@
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"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:",
"titleManageServers": "Manage Servers",
"dateNever": "Never",

View File

@ -1,6 +1,7 @@
{
"@@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",
"settingTheme": "Utilisez des thèmes clairs",
"torSettingsUseCustomTorServiceConfiguration": "Utiliser une configuration personnalisée du service Tor (torrc)",
@ -213,7 +214,7 @@
"dateNever": "Jamais",
"titleManageServers": "Gérer les serveurs",
"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.",
"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.",

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
@ -38,7 +39,7 @@
"copiedToClipboardNotification": "Copiato negli Appunti",
"groupNameLabel": "Nome del gruppo",
"titleManageServers": "Gestisci i Server",
"leaveGroup": "Lascia Questa Conversazione",
"leaveConversation": "Lascia Questa Conversazione",
"yesLeave": "Sì, Lascia Questa Conversazione",
"newPassword": "Nuova Password",
"sendMessage": "Invia Messaggio",

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"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.",
"yesLeave": "Opuść",
"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:",
"dateNever": "Nigdy",
"dateLastYear": "Rok temu",

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
@ -148,7 +149,7 @@
"newPassword": "New Password",
"yesLeave": "Yes, Leave This Conversation",
"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:",
"titleManageServers": "Manage Servers",
"dateNever": "Never",

View File

@ -1,6 +1,7 @@
{
"@@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.",
"torSettingsEnableCache": "Cache Tor Consensus",
"labelTorNetwork": "Tor Network",
@ -164,7 +165,7 @@
"newPassword": "Новый пароль",
"yesLeave": "Да, оставить этот чат",
"reallyLeaveThisGroupPrompt": "Вы уверены, что хотите закончить этот разговор? Все сообщения будут удалены.",
"leaveGroup": "Да, оставить этот чат",
"leaveConversation": "Да, оставить этот чат",
"inviteToGroup": "Вас пригласили присоединиться к группе:",
"titleManageServers": "Управление серверами",
"dateNever": "Никогда",

View File

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

View File

@ -14,9 +14,11 @@ class ProfileInfoState extends ChangeNotifier {
final String onion;
String _nickname = "";
String _imagePath = "";
String _defaultImagePath = "";
int _unreadMessages = 0;
bool _online = false;
Map<String, FileDownloadProgress> _downloads = Map<String, FileDownloadProgress>();
Map<String, int> _downloadTriggers = Map<String, int>();
// assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password.
@ -26,14 +28,17 @@ class ProfileInfoState extends ChangeNotifier {
required this.onion,
nickname = "",
imagePath = "",
defaultImagePath = "",
unreadMessages = 0,
contactsJson = "",
serversJson = "",
online = false,
encrypted = true,
String,
}) {
this._nickname = nickname;
this._imagePath = imagePath;
this._defaultImagePath = defaultImagePath;
this._unreadMessages = unreadMessages;
this._online = online;
this._encrypted = encrypted;
@ -49,6 +54,7 @@ class ProfileInfoState extends ChangeNotifier {
nickname: contact["name"],
status: contact["status"],
imagePath: contact["picture"],
defaultImagePath: contact["isGroup"] ? contact["picture"] : contact["defaultPicture"],
accepted: contact["accepted"],
blocked: contact["blocked"],
savePeerHistory: contact["saveConversationHistory"],
@ -114,6 +120,12 @@ class ProfileInfoState extends ChangeNotifier {
notifyListeners();
}
String get defaultImagePath => this._defaultImagePath;
set defaultImagePath(String newVal) {
this._defaultImagePath = newVal;
notifyListeners();
}
int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) {
this._unreadMessages = newVal;
@ -160,6 +172,7 @@ class ProfileInfoState extends ChangeNotifier {
contact["identifier"],
contact["onion"],
nickname: contact["name"],
defaultImagePath: contact["defaultPicture"],
status: contact["status"],
imagePath: contact["picture"],
accepted: contact["accepted"],
@ -178,22 +191,14 @@ class ProfileInfoState extends ChangeNotifier {
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) {
unreadMessages++;
notifyListeners();
}
contactList.newMessage(
identifier,
messageID,
timestamp,
senderHandle,
senderImage,
isAuto,
data,
contenthash,
selectedConversation);
contactList.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
}
void downloadInit(String fileKey, int numChunks) {
@ -232,6 +237,15 @@ class ProfileInfoState extends ChangeNotifier {
// so setting numChunks correctly shouldn't matter
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
if (!this._downloads[fileKey]!.complete) {
this._downloads[fileKey]!.timeEnd = DateTime.now();
@ -308,4 +322,9 @@ class ProfileInfoState extends ChangeNotifier {
}
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 = [];
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);
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 {
_profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online);
}
@ -28,7 +29,5 @@ class ProfileListState extends ChangeNotifier {
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:math';
import 'package:cwtch/config.dart';
import 'package:cwtch/cwtch/cwtch.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/controllers/filesharing.dart' as filesharing;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cwtch/widgets/buttontextfield.dart';
@ -15,6 +15,7 @@ import 'package:cwtch/widgets/textfield.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../constants.dart';
import '../cwtch_icons_icons.dart';
import '../errorHandler.dart';
import '../main.dart';
@ -89,14 +90,39 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
Visibility(
visible: Provider.of<ProfileInfoState>(context).onion.isNotEmpty,
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
ProfileImage(
imagePath: Provider.of<ProfileInfoState>(context).imagePath,
diameter: 120,
maskOut: false,
border: theme.theme.portraitOnlineBorderColor,
badgeTextColor: Colors.red,
badgeColor: Colors.red,
)
MouseRegion(
cursor: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic,
child: GestureDetector(
// don't allow setting of profile images if the image previews experiment is disabled.
onTap: Provider.of<AppState>(context, listen: false).disableFilePicker || !Provider.of<Settings>(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment)
? null
: () {
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: [
CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel),

View File

@ -86,11 +86,10 @@ class _ContactsViewState extends State<ContactsView> {
Navigator.of(context).pop();
},
),
StreamBuilder<bool>(
stream: Provider.of<AppState>(context).getUnreadProfileNotifyStream(),
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(
visible: unreadCount > 0,
@ -104,7 +103,9 @@ class _ContactsViewState extends State<ContactsView> {
title: RepaintBoundary(
child: Row(children: [
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,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
badgeTextColor: Colors.red,

View File

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

View File

@ -159,7 +159,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
),
Row(crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [
Tooltip(
message: AppLocalizations.of(context)!.leaveGroup,
message: AppLocalizations.of(context)!.leaveConversation,
child: TextButton.icon(
onPressed: () {
showAlertDialog(context);
@ -167,7 +167,7 @@ class _GroupSettingsViewState extends State<GroupSettingsView> {
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.transparent)),
icon: Icon(CwtchIcons.leave_group),
label: Text(
AppLocalizations.of(context)!.leaveGroup,
AppLocalizations.of(context)!.leaveConversation,
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/messageloadingbubble.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:cwtch/controllers/filesharing.dart' as filesharing;
import 'package:file_picker/file_picker.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:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../constants.dart';
import '../main.dart';
import '../settings.dart';
import '../widgets/messagelist.dart';
@ -92,7 +93,16 @@ class _MessageViewState extends State<MessageView> {
onPressed: Provider.of<AppState>(context).disableFilePicker
? 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,
title: Row(children: [
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,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor,
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 {
showModalBottomSheet<void>(
context: ctx,

View File

@ -3,6 +3,7 @@ import 'dart:ui';
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/widgets/buttontextfield.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
});
},
icon: Icon(CwtchIcons.leave_chat),
icon: Icon(Icons.archive),
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),
onPressed: () {
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...
Provider.of<ContactInfoState>(context, listen: false).isArchived = true;
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, handle);
Provider.of<ProfileInfoState>(context, listen: false).contactList.removeContact(identifier);
Provider.of<FlwtchState>(context, listen: false).cwtch.DeleteContact(profileOnion, identifier);
Future.delayed(Duration(milliseconds: 500), () {
Provider.of<AppState>(context, listen: false).selectedConversation = null;
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,
badgeTextColor: Provider.of<Settings>(context).theme.portraitContactBadgeTextColor,
diameter: 64.0,
imagePath: contact.imagePath,
imagePath: Provider.of<Settings>(context).isExperimentEnabled(ImagePreviewsExperiment) ? contact.imagePath : contact.defaultImagePath,
maskOut: !contact.isOnline(),
border: contact.isOnline()
? Provider.of<Settings>(context).theme.portraitOnlineBorderColor

View File

@ -152,6 +152,12 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
];
} else {
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(
onTap: !isGroup
? null
@ -162,7 +168,8 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
padding: EdgeInsets.all(4.0),
child: ProfileImage(
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,
badgeTextColor: Colors.red,
badgeColor: Colors.red,

View File

@ -1,3 +1,6 @@
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:cwtch/themes/opaque.dart';
import 'package:provider/provider.dart';
@ -6,7 +9,15 @@ import '../settings.dart';
class ProfileImage extends StatefulWidget {
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 String imagePath;
final Color border;
@ -14,6 +25,7 @@ class ProfileImage extends StatefulWidget {
final Color badgeColor;
final Color badgeTextColor;
final bool maskOut;
final bool badgeEdit;
final String tooltip;
@override
@ -23,8 +35,11 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> {
@override
Widget build(BuildContext context) {
var image = Image(
image: AssetImage("assets/" + widget.imagePath),
var file = new File(widget.imagePath);
var image = Image.file(
file,
cacheWidth: 512,
cacheHeight: 512,
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: !widget.maskOut
@ -36,6 +51,21 @@ class _ProfileImageState extends State<ProfileImage> {
isAntiAlias: true,
width: 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(
@ -50,14 +80,19 @@ class _ProfileImageState extends State<ProfileImage> {
padding: const EdgeInsets.all(2.0), //border size
child: ClipOval(clipBehavior: Clip.antiAlias, child: widget.tooltip == "" ? image : Tooltip(message: widget.tooltip, child: image))))),
Visibility(
visible: widget.badgeCount > 0,
visible: widget.badgeEdit || widget.badgeCount > 0,
child: Positioned(
bottom: 0.0,
right: 0.0,
child: CircleAvatar(
radius: 10.0,
radius: max(10.0, widget.diameter / 6.0),
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,
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor,
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)),
Expanded(
child: Column(
@ -105,11 +105,12 @@ class _ProfileRowState extends State<ProfileRow> {
void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Provider.of<ErrorHandler>(context, listen: false).reset();
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
builder: (BuildContext bcontext) {
var profile = Provider.of<FlwtchState>(bcontext).profs.getProfile(onion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted),
ChangeNotifierProvider<ProfileInfoState>.value(
value: profile,
),
],
builder: (context, widget) => AddEditProfileView(),

View File

@ -8,7 +8,8 @@ doNothing(String x) {}
// Provides a styled Text Field for use in Form Widgets.
// Callers must provide a text controller, label helper text and a validator.
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 String hintText;
final FormFieldValidator? validator;