From 3d85883f8e1008bf51bbc00819ca04956d969cf0 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Fri, 4 Feb 2022 16:57:31 -0800 Subject: [PATCH 1/9] Profile Images --- .../kotlin/im/cwtch/flwtch/FlwtchWorker.kt | 2 +- lib/cwtch/cwtchNotifier.dart | 27 ++++++--- lib/models/profile.dart | 29 ++++++---- lib/models/profilelist.dart | 4 +- lib/views/addeditprofileview.dart | 55 ++++++++++++++++--- lib/views/contactsview.dart | 3 +- lib/widgets/messagerow.dart | 4 +- lib/widgets/profileimage.dart | 24 +++++++- lib/widgets/profilerow.dart | 7 ++- lib/widgets/textfield.dart | 3 +- 10 files changed, 119 insertions(+), 39 deletions(-) diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt index cf207e99..44ba19f3 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/FlwtchWorker.kt @@ -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) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index fee09984..cd0d564f 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -106,7 +106,7 @@ 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"], nickname: data["GroupName"], status: status, server: data["GroupServer"], @@ -147,7 +147,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 +199,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 +301,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 +322,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']}"); } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index ee10ec47..75b37f0b 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -17,6 +17,7 @@ class ProfileInfoState extends ChangeNotifier { int _unreadMessages = 0; bool _online = false; Map _downloads = Map(); + Map _downloadTriggers = Map(); // assume profiles are encrypted...this will be set to false // in the constructor if the profile is encrypted with the defacto password. @@ -178,22 +179,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 +225,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 +310,9 @@ class ProfileInfoState extends ChangeNotifier { } return prettyBytes((bytes / seconds).round()) + "/s"; } + + void waitForDownloadComplete(int identifier, String fileKey) { + _downloadTriggers[fileKey] = identifier; + notifyListeners(); + } } diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart index 8cae18b5..e3766e01 100644 --- a/lib/models/profilelist.dart +++ b/lib/models/profilelist.dart @@ -28,7 +28,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); } diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 9841712b..07b2968c 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -4,7 +4,9 @@ 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:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cwtch/widgets/buttontextfield.dart'; @@ -66,6 +68,37 @@ class _AddEditProfileViewState extends State { }); } + void _showFilePicker(BuildContext ctx) 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(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) { + var profile = Provider.of(context, listen: false).onion; + // Share this image publically (conversation handle == -1) + Provider.of(context, listen: false).cwtch.ShareFile(profile, -1, file.path); + // update the image cache locally + Provider.of(context, listen: false).imagePath = file.path; + } else { + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.msgFileTooBig), + duration: Duration(seconds: 4), + ); + ScaffoldMessenger.of(ctx).showSnackBar(snackBar); + } + } + } + // A few implementation notes // We use Visibility to hide optional structures when they are not requested. // We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together. @@ -89,14 +122,20 @@ class _AddEditProfileViewState extends State { Visibility( visible: Provider.of(context).onion.isNotEmpty, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - ProfileImage( - imagePath: Provider.of(context).imagePath, - diameter: 120, - maskOut: false, - border: theme.theme.portraitOnlineBorderColor, - badgeTextColor: Colors.red, - badgeColor: Colors.red, - ) + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + _showFilePicker(context); + }, + child: ProfileImage( + imagePath: Provider.of(context).imagePath, + diameter: 120, + maskOut: false, + border: theme.theme.portraitOnlineBorderColor, + badgeTextColor: Colors.red, + badgeColor: Colors.red, + ))) ])), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index d48ffada..317a63b6 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -86,11 +86,10 @@ class _ContactsViewState extends State { Navigator.of(context).pop(); }, ), - StreamBuilder( stream: Provider.of(context).getUnreadProfileNotifyStream(), builder: (BuildContext context, AsyncSnapshot unreadCountSnapshot) { - int unreadCount = Provider.of(context).generateUnreadCount(Provider.of(context).selectedProfile ?? "") ; + int unreadCount = Provider.of(context).generateUnreadCount(Provider.of(context).selectedProfile ?? ""); return Visibility( visible: unreadCount > 0, diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 577afb4f..feffa1d2 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -152,6 +152,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi ]; } else { var contact = Provider.of(context); + ContactInfoState? sender = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); Widget wdgPortrait = GestureDetector( onTap: !isGroup ? null @@ -162,7 +163,8 @@ class MessageRowState extends State with SingleTickerProviderStateMi padding: EdgeInsets.all(4.0), child: ProfileImage( diameter: 48.0, - imagePath: Provider.of(context).senderImage ?? contact.imagePath, + // default to the contact image...otherwise use a derived sender image... + imagePath: sender?.imagePath ?? Provider.of(context).senderImage!, border: contact.status == "Authenticated" ? Provider.of(context).theme.portraitOnlineBorderColor : Provider.of(context).theme.portraitOfflineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red, diff --git a/lib/widgets/profileimage.dart b/lib/widgets/profileimage.dart index f790a520..8cea2c9c 100644 --- a/lib/widgets/profileimage.dart +++ b/lib/widgets/profileimage.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:cwtch/themes/opaque.dart'; import 'package:provider/provider.dart'; @@ -23,8 +25,11 @@ class ProfileImage extends StatefulWidget { class _ProfileImageState extends State { @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 +41,21 @@ class _ProfileImageState extends State { 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(context).theme.mode == mode_dark + ? BlendMode.softLight + : BlendMode.darken + : BlendMode.srcOut, + color: Provider.of(context).theme.portraitBackgroundColor, + isAntiAlias: true, + width: widget.diameter, + height: widget.diameter); + }, ); return RepaintBoundary( diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 770dbe54..6808009d 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -105,11 +105,12 @@ class _ProfileRowState extends State { void _pushEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) { Provider.of(context, listen: false).reset(); Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) { + builder: (BuildContext bcontext) { + var profile = Provider.of(bcontext).profs.getProfile(onion)!; return MultiProvider( providers: [ - ChangeNotifierProvider( - create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted), + ChangeNotifierProvider.value( + value: profile, ), ], builder: (context, widget) => AddEditProfileView(), diff --git a/lib/widgets/textfield.dart b/lib/widgets/textfield.dart index 793077ff..0a9ffa6d 100644 --- a/lib/widgets/textfield.dart +++ b/lib/widgets/textfield.dart @@ -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; From c6192ef7367e59866d9ec168affc751fbc74c644 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 11:30:17 -0800 Subject: [PATCH 2/9] Factor out showFilePicker into a generic controller --- lib/constants.dart | 3 ++ lib/controllers/filesharing.dart | 30 +++++++++++++++++ lib/views/addeditprofileview.dart | 56 +++++++++++-------------------- lib/views/messageview.dart | 44 +++++++----------------- 4 files changed, 64 insertions(+), 69 deletions(-) create mode 100644 lib/constants.dart create mode 100644 lib/controllers/filesharing.dart diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 00000000..04abe41d --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,3 @@ +const int MaxImageFileSharingSize = 20971520; + +const int MaxGeneralImageFileSharingSize = 10737418240; diff --git a/lib/controllers/filesharing.dart b/lib/controllers/filesharing.dart new file mode 100644 index 00000000..8696af29 --- /dev/null +++ b/lib/controllers/filesharing.dart @@ -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 onCancel, Function onError) 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(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(); + } +} diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 07b2968c..b707f52f 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -1,12 +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:file_picker/file_picker.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'; @@ -17,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'; @@ -68,37 +67,6 @@ class _AddEditProfileViewState extends State { }); } - void _showFilePicker(BuildContext ctx) 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(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) { - var profile = Provider.of(context, listen: false).onion; - // Share this image publically (conversation handle == -1) - Provider.of(context, listen: false).cwtch.ShareFile(profile, -1, file.path); - // update the image cache locally - Provider.of(context, listen: false).imagePath = file.path; - } else { - final snackBar = SnackBar( - content: Text(AppLocalizations.of(context)!.msgFileTooBig), - duration: Duration(seconds: 4), - ); - ScaffoldMessenger.of(ctx).showSnackBar(snackBar); - } - } - } - // A few implementation notes // We use Visibility to hide optional structures when they are not requested. // We used SizedBox for inter-widget height padding in columns, otherwise elements can render a little too close together. @@ -125,9 +93,23 @@ class _AddEditProfileViewState extends State { MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: () { - _showFilePicker(context); - }, + onTap: Provider.of(context).disableFilePicker + ? null + : () { + filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) { + var profile = Provider.of(context, listen: false).onion; + // Share this image publicly (conversation handle == -1) + Provider.of(context, listen: false).cwtch.ShareFile(profile, -1, file.path); + // update the image cache locally + Provider.of(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(context).imagePath, diameter: 120, diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 8db678ff..d5d9a01c 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -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 { onPressed: Provider.of(context).disableFilePicker ? null : () { - _showFilePicker(context); + imagePreview = null; + filesharing.showFilePicker(context, MaxGeneralImageFileSharingSize, (File file) { + _confirmFileSend(context, file.path); + }, () { + final snackBar = SnackBar( + content: Text(AppLocalizations.of(context)!.msgFileTooBig), + duration: Duration(seconds: 4), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, () {}); }, )); } @@ -415,36 +425,6 @@ class _MessageViewState extends State { }); } - 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(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( context: ctx, From d158d7d61919753894cc9180039c2e418f8bcbed Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 12:20:54 -0800 Subject: [PATCH 3/9] Select Profile Image tooltip + restrict selection only when image previews are enabled --- lib/controllers/filesharing.dart | 2 +- lib/l10n/intl_de.arb | 5 +++-- lib/l10n/intl_en.arb | 5 +++-- lib/l10n/intl_es.arb | 5 +++-- lib/l10n/intl_fr.arb | 5 +++-- lib/l10n/intl_it.arb | 5 +++-- lib/l10n/intl_pl.arb | 5 +++-- lib/l10n/intl_pt.arb | 5 +++-- lib/l10n/intl_ru.arb | 5 +++-- lib/views/addeditprofileview.dart | 6 ++++-- lib/views/groupsettingsview.dart | 4 ++-- 11 files changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/controllers/filesharing.dart b/lib/controllers/filesharing.dart index 8696af29..cbfdaa20 100644 --- a/lib/controllers/filesharing.dart +++ b/lib/controllers/filesharing.dart @@ -4,7 +4,7 @@ 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 onCancel, Function onError) async { +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. diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 5ac7c7d8..cbea3bfd 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -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", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index df6497f1..6f481895 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 5f418b75..ff93defc 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -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", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index dd2c0544..7e8e365b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -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.", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index b94f3e24..ac17d6b7 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -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", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 3866a59b..cf32bf98 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -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", diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb index 41142267..a8350528 100644 --- a/lib/l10n/intl_pt.arb +++ b/lib/l10n/intl_pt.arb @@ -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", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 5357f2cb..be2172b4 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -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": "Никогда", diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index b707f52f..3a054f4f 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -91,9 +91,10 @@ class _AddEditProfileViewState extends State { visible: Provider.of(context).onion.isNotEmpty, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ MouseRegion( - cursor: SystemMouseCursors.click, + cursor: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic, child: GestureDetector( - onTap: Provider.of(context).disableFilePicker + // don't allow setting of profile images if the image previews experiment is disabled. + onTap: Provider.of(context).disableFilePicker || !Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? null : () { filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) { @@ -113,6 +114,7 @@ class _AddEditProfileViewState extends State { child: ProfileImage( imagePath: Provider.of(context).imagePath, diameter: 120, + tooltip: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", maskOut: false, border: theme.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, diff --git a/lib/views/groupsettingsview.dart b/lib/views/groupsettingsview.dart index 18855e7e..1026bf05 100644 --- a/lib/views/groupsettingsview.dart +++ b/lib/views/groupsettingsview.dart @@ -159,7 +159,7 @@ class _GroupSettingsViewState extends State { ), 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 { 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), ), )) From 2a2d808b605da6a7e9b90fdc916b30dab905c54c Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 12:23:26 -0800 Subject: [PATCH 4/9] Disable image previews when file sharing is disables --- lib/views/globalsettingsview.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 71e8a5a3..71eea58c 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -251,6 +251,7 @@ class _GlobalSettingsViewState extends State { settings.enableExperiment(FileSharingExperiment); } else { settings.disableExperiment(FileSharingExperiment); + settings.disableExperiment(ImagePreviewsExperiment); } saveSettings(context); }, From b280765631abc657a9e8a6df06db3499fae3e4cc Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 14:26:14 -0800 Subject: [PATCH 5/9] Fallback to Default Profile Images when Image Previews are Disabled --- lib/cwtch/cwtchNotifier.dart | 4 +++- lib/models/contact.dart | 10 ++++++++++ lib/models/profile.dart | 12 ++++++++++++ lib/models/profilelist.dart | 5 +++-- lib/views/addeditprofileview.dart | 7 +++++-- lib/views/contactsview.dart | 4 +++- lib/views/messageview.dart | 4 +++- lib/widgets/contactrow.dart | 2 +- lib/widgets/messagerow.dart | 8 +++++++- lib/widgets/profilerow.dart | 2 +- 10 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index cd0d564f..055b781d 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -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"], @@ -107,6 +108,7 @@ class CwtchNotifier { blocked: false, // we created accepted: true, // we created imagePath: data["picture"], + defaultImagePath: data["picture"], nickname: data["GroupName"], status: status, server: data["GroupServer"], diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 7f840747..38cd31cd 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -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) { diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 75b37f0b..070a25cc 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -14,6 +14,7 @@ class ProfileInfoState extends ChangeNotifier { final String onion; String _nickname = ""; String _imagePath = ""; + String _defaultImagePath = ""; int _unreadMessages = 0; bool _online = false; Map _downloads = Map(); @@ -27,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; @@ -50,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"], @@ -115,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; @@ -161,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"], diff --git a/lib/models/profilelist.dart b/lib/models/profilelist.dart index e3766e01..53a12384 100644 --- a/lib/models/profilelist.dart +++ b/lib/models/profilelist.dart @@ -6,10 +6,11 @@ class ProfileListState extends ChangeNotifier { List _profiles = []; int get num => _profiles.length; - void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online, bool encrypted) { + 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); } diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 3a054f4f..ef398ffd 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -112,9 +112,12 @@ class _AddEditProfileViewState extends State { }, () {}); }, child: ProfileImage( - imagePath: Provider.of(context).imagePath, + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? Provider.of(context).imagePath + : Provider.of(context).defaultImagePath, diameter: 120, - tooltip: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", + tooltip: + Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", maskOut: false, border: theme.theme.portraitOnlineBorderColor, badgeTextColor: Colors.red, diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 317a63b6..ad3a5c8f 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -103,7 +103,9 @@ class _ContactsViewState extends State { title: RepaintBoundary( child: Row(children: [ ProfileImage( - imagePath: Provider.of(context).imagePath, + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? Provider.of(context).imagePath + : Provider.of(context).defaultImagePath, diameter: 42, border: Provider.of(context).current().portraitOnlineBorderColor, badgeTextColor: Colors.red, diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index d5d9a01c..5ef99e48 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -139,7 +139,9 @@ class _MessageViewState extends State { leading: Provider.of(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null, title: Row(children: [ ProfileImage( - imagePath: Provider.of(context).imagePath, + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? Provider.of(context).imagePath + : Provider.of(context).defaultImagePath, diameter: 42, border: Provider.of(context).current().portraitOnlineBorderColor, badgeTextColor: Colors.red, diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index cdad4f77..6cbc6ef7 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -50,7 +50,7 @@ class _ContactRowState extends State { badgeColor: Provider.of(context).theme.portraitContactBadgeColor, badgeTextColor: Provider.of(context).theme.portraitContactBadgeTextColor, diameter: 64.0, - imagePath: contact.imagePath, + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? contact.imagePath : contact.defaultImagePath, maskOut: !contact.isOnline(), border: contact.isOnline() ? Provider.of(context).theme.portraitOnlineBorderColor diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index feffa1d2..0deb647b 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -153,6 +153,12 @@ class MessageRowState extends State with SingleTickerProviderStateMi } else { var contact = Provider.of(context); ContactInfoState? sender = Provider.of(context).contactList.findContact(Provider.of(context).senderHandle); + + String imagePath = Provider.of(context).senderImage!; + if (sender != null) { + imagePath = + Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath; + } Widget wdgPortrait = GestureDetector( onTap: !isGroup ? null @@ -164,7 +170,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi child: ProfileImage( diameter: 48.0, // default to the contact image...otherwise use a derived sender image... - imagePath: sender?.imagePath ?? Provider.of(context).senderImage!, + imagePath: imagePath, border: contact.status == "Authenticated" ? Provider.of(context).theme.portraitOnlineBorderColor : Provider.of(context).theme.portraitOfflineBorderColor, badgeTextColor: Colors.red, badgeColor: Colors.red, diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart index 6808009d..9c2a576b 100644 --- a/lib/widgets/profilerow.dart +++ b/lib/widgets/profilerow.dart @@ -38,7 +38,7 @@ class _ProfileRowState extends State { badgeColor: Provider.of(context).theme.portraitProfileBadgeColor, badgeTextColor: Provider.of(context).theme.portraitProfileBadgeTextColor, diameter: 64.0, - imagePath: profile.imagePath, + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? profile.imagePath : profile.defaultImagePath, border: profile.isOnline ? Provider.of(context).theme.portraitOnlineBorderColor : Provider.of(context).theme.portraitOfflineBorderColor)), Expanded( child: Column( From 5b5fe586e804cc18850c5a41f7e97885ad294d44 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 14:53:33 -0800 Subject: [PATCH 6/9] Update Lib Cwtch, Allow Deleting P2P contacts, Formatting --- LIBCWTCH-GO-MACOS.version | 2 +- LIBCWTCH-GO.version | 2 +- lib/views/peersettingsview.dart | 29 ++++++++++++++++++++++++----- lib/widgets/messagerow.dart | 3 +-- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/LIBCWTCH-GO-MACOS.version b/LIBCWTCH-GO-MACOS.version index 1f70ab7a..131da5bc 100644 --- a/LIBCWTCH-GO-MACOS.version +++ b/LIBCWTCH-GO-MACOS.version @@ -1 +1 @@ -2022-02-04-16-57-v1.5.4-28-g4e4e331 \ No newline at end of file +2022-02-07-17-39-v1.5.4-31-g17acc3b \ No newline at end of file diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index 61cc1a7b..ae1b1c97 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2022-02-04-21-58-v1.5.4-28-g4e4e331 \ No newline at end of file +2022-02-07-22-31-v1.5.4-31-g17acc3b \ No newline at end of file diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 0ab762ee..0206c175 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -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 { 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 { child: Text(AppLocalizations.of(context)!.yesLeave), onPressed: () { var profileOnion = Provider.of(context, listen: false).profileOnion; - var handle = Provider.of(context, listen: false).identifier; + var identifier = Provider.of(context, listen: false).identifier; // locally update cache... - Provider.of(context, listen: false).isArchived = true; - Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, handle); + Provider.of(context, listen: false).contactList.removeContact(identifier); + Provider.of(context, listen: false).cwtch.DeleteContact(profileOnion, identifier); Future.delayed(Duration(milliseconds: 500), () { Provider.of(context, listen: false).selectedConversation = null; Navigator.of(context).popUntil((route) => route.settings.name == "conversations"); // dismiss dialog diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index 0deb647b..ad92f840 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -156,8 +156,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi String imagePath = Provider.of(context).senderImage!; if (sender != null) { - imagePath = - Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath; + imagePath = Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? sender.imagePath : sender.defaultImagePath; } Widget wdgPortrait = GestureDetector( onTap: !isGroup From d902ba5cced7eac5fc442b7cec35bff88ea35ae8 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 14:59:09 -0800 Subject: [PATCH 7/9] Rename Constant --- lib/constants.dart | 2 +- lib/views/messageview.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 04abe41d..1edea436 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,3 +1,3 @@ const int MaxImageFileSharingSize = 20971520; -const int MaxGeneralImageFileSharingSize = 10737418240; +const int MaxGeneralFileSharingSize = 10737418240; diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 5ef99e48..3630034d 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -94,7 +94,7 @@ class _MessageViewState extends State { ? null : () { imagePreview = null; - filesharing.showFilePicker(context, MaxGeneralImageFileSharingSize, (File file) { + filesharing.showFilePicker(context, MaxGeneralFileSharingSize, (File file) { _confirmFileSend(context, file.path); }, () { final snackBar = SnackBar( From 403454d6b8378d4125ea5d88683b62f573086aaf Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 15:12:36 -0800 Subject: [PATCH 8/9] Add Edit Badge --- lib/views/addeditprofileview.dart | 26 +++++++++++++------------- lib/widgets/profileimage.dart | 23 +++++++++++++++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index ef398ffd..4a266ce6 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -91,10 +91,10 @@ class _AddEditProfileViewState extends State { visible: Provider.of(context).onion.isNotEmpty, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ MouseRegion( - cursor: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic, + cursor: Provider.of(context, listen: false).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(context).disableFilePicker || !Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + onTap: Provider.of(context).disableFilePicker || !Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) ? null : () { filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) { @@ -112,17 +112,17 @@ class _AddEditProfileViewState extends State { }, () {}); }, child: ProfileImage( - imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) - ? Provider.of(context).imagePath - : Provider.of(context).defaultImagePath, - diameter: 120, - tooltip: - Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", - maskOut: false, - border: theme.theme.portraitOnlineBorderColor, - badgeTextColor: Colors.red, - badgeColor: Colors.red, - ))) + imagePath: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) + ? Provider.of(context).imagePath + : Provider.of(context).defaultImagePath, + diameter: 120, + tooltip: + Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment) ? AppLocalizations.of(context)!.tooltipSelectACustomProfileImage : "", + maskOut: false, + border: theme.theme.portraitOnlineBorderColor, + badgeTextColor: theme.theme.portraitContactBadgeTextColor, + badgeColor: theme.theme.portraitContactBadgeColor, + badgeEdit: Provider.of(context).isExperimentEnabled(ImagePreviewsExperiment)))) ])), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ CwtchLabel(label: AppLocalizations.of(context)!.displayNameLabel), diff --git a/lib/widgets/profileimage.dart b/lib/widgets/profileimage.dart index 8cea2c9c..db2c7774 100644 --- a/lib/widgets/profileimage.dart +++ b/lib/widgets/profileimage.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:cwtch/themes/opaque.dart'; @@ -8,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; @@ -16,6 +25,7 @@ class ProfileImage extends StatefulWidget { final Color badgeColor; final Color badgeTextColor; final bool maskOut; + final bool badgeEdit; final String tooltip; @override @@ -70,14 +80,19 @@ class _ProfileImageState extends State { 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)), ), )), ])); From bf4cfde7dfe4085259b92807e9e664dcc10128bb Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Mon, 7 Feb 2022 15:16:02 -0800 Subject: [PATCH 9/9] Fixup Context Listen --- lib/views/addeditprofileview.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/views/addeditprofileview.dart b/lib/views/addeditprofileview.dart index 4a266ce6..0670058a 100644 --- a/lib/views/addeditprofileview.dart +++ b/lib/views/addeditprofileview.dart @@ -91,10 +91,10 @@ class _AddEditProfileViewState extends State { visible: Provider.of(context).onion.isNotEmpty, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ MouseRegion( - cursor: Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) ? SystemMouseCursors.click : SystemMouseCursors.basic, + cursor: Provider.of(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(context).disableFilePicker || !Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) + onTap: Provider.of(context, listen: false).disableFilePicker || !Provider.of(context, listen: false).isExperimentEnabled(ImagePreviewsExperiment) ? null : () { filesharing.showFilePicker(context, MaxImageFileSharingSize, (File file) {