Profile Images
continuous-integration/drone/pr Build is pending
Details
continuous-integration/drone/pr Build is pending
Details
This commit is contained in:
parent
e22db92dc1
commit
3d85883f8e
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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']}");
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
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.
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<AddEditProfileView> {
|
|||
});
|
||||
}
|
||||
|
||||
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<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) {
|
||||
var profile = Provider.of<ProfileInfoState>(context, listen: false).onion;
|
||||
// Share this image publically (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;
|
||||
} 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<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: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
_showFilePicker(context);
|
||||
},
|
||||
child: ProfileImage(
|
||||
imagePath: Provider.of<ProfileInfoState>(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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -152,6 +152,7 @@ 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);
|
||||
Widget wdgPortrait = GestureDetector(
|
||||
onTap: !isGroup
|
||||
? null
|
||||
|
@ -162,7 +163,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: sender?.imagePath ?? Provider.of<MessageMetadata>(context).senderImage!,
|
||||
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor : Provider.of<Settings>(context).theme.portraitOfflineBorderColor,
|
||||
badgeTextColor: Colors.red,
|
||||
badgeColor: Colors.red,
|
||||
|
|
|
@ -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<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 +41,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(
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue