Merge pull request 'model contact uses authorization now; add tooltips to contact pics in chat; dual action: add or goto' (#105) from authTooltip into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #105
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
This commit is contained in:
Sarah Jamie Lewis 2021-07-12 14:52:44 -07:00
commit 6ea793875f
15 changed files with 194 additions and 98 deletions

View File

@ -49,8 +49,7 @@ class CwtchNotifier {
nickname: data["nick"], nickname: data["nick"],
status: data["status"], status: data["status"],
imagePath: data["picture"], imagePath: data["picture"],
isBlocked: data["authorization"] == "blocked", authorization: stringToContactAuthorization(data["authorization"]),
isInvitation: data["authorization"] == "unknown",
savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"], savePeerHistory: data["saveConversationHistory"] == null ? "DeleteHistoryConfirmed" : data["saveConversationHistory"],
numMessages: int.parse(data["numMessages"]), numMessages: int.parse(data["numMessages"]),
numUnread: int.parse(data["unread"]), numUnread: int.parse(data["unread"]),
@ -69,7 +68,7 @@ class CwtchNotifier {
} }
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], data["GroupID"],
isInvitation: false, imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now())); authorization: ContactAuthorization.approved, imagePath: data["PicturePath"], nickname: data["GroupName"], status: status, server: data["GroupServer"], isGroup: true, lastMessageTime: DateTime.now()));
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
} }
break; break;
@ -91,8 +90,7 @@ class CwtchNotifier {
contact.status = data["ConnectionState"]; contact.status = data["ConnectionState"];
} }
if (data["authorization"] != null) { if (data["authorization"] != null) {
contact.isInvitation = data["authorization"] == "unknown"; contact.authorization = stringToContactAuthorization(data["authorization"]);
contact.isBlocked = data["authorization"] == "blocked";
} }
// contact.[status/isBlocked] might change the list's sort order // contact.[status/isBlocked] might change the list's sort order
profileCN.getProfile(data["ProfileOnion"])?.contactList.resort(); profileCN.getProfile(data["ProfileOnion"])?.contactList.resort();
@ -241,7 +239,7 @@ class CwtchNotifier {
if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) { if (profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(groupInvite["GroupID"]) == null) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"], profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(data["ProfileOnion"], groupInvite["GroupID"],
isInvitation: false, authorization: ContactAuthorization.approved,
imagePath: data["PicturePath"], imagePath: data["PicturePath"],
nickname: groupInvite["GroupName"], nickname: groupInvite["GroupName"],
server: groupInvite["ServerHost"], server: groupInvite["ServerHost"],
@ -255,7 +253,7 @@ class CwtchNotifier {
case "AcceptGroupInvite": case "AcceptGroupInvite":
EnvironmentConfig.debugLog("accept group invite"); EnvironmentConfig.debugLog("accept group invite");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.isInvitation = false; profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.authorization = ContactAuthorization.approved;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now()); profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
break; break;
case "ServerStateChange": case "ServerStateChange":

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "de", "@@locale": "de",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -120,7 +123,7 @@
"password1Label": "Passwort", "password1Label": "Passwort",
"currentPasswordLabel": "aktuelles Passwort", "currentPasswordLabel": "aktuelles Passwort",
"yourDisplayName": "Dein Anzeigename", "yourDisplayName": "Dein Anzeigename",
"profileOnionLabel": "Sende diese Adresse an andere Nutzer, mit denen du dich verbinden möchtest", "profileOnionLabel": "Senden Sie diese Adresse an Peers, mit denen Sie sich verbinden möchten",
"noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.", "noPasswordWarning": "Wenn für dieses Konto kein Passwort verwendet wird, bedeutet dies, dass alle lokal gespeicherten Daten nicht verschlüsselt werden.",
"radioNoPassword": "Unverschlüsselt (kein Passwort)", "radioNoPassword": "Unverschlüsselt (kein Passwort)",
"radioUsePassword": "Passwort", "radioUsePassword": "Passwort",
@ -132,13 +135,13 @@
"profileName": "Anzeigename", "profileName": "Anzeigename",
"editProfileTitle": "Profil bearbeiten", "editProfileTitle": "Profil bearbeiten",
"addProfileTitle": "Neues Profil hinzufügen", "addProfileTitle": "Neues Profil hinzufügen",
"deleteBtn": "löschen", "deleteBtn": "Löschen",
"unblockBtn": "Anderen Nutzer entsperren", "unblockBtn": "Anderen Nutzer entsperren",
"dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen", "dontSavePeerHistory": "Verlauf mit anderem Nutzer löschen",
"savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.", "savePeerHistoryDescription": "Legt fest, ob ein mit dem anderen Nutzer verknüpfter Verlauf gelöscht werden soll oder nicht.",
"savePeerHistory": "Peer-Verlauf speichern", "savePeerHistory": "Peer-Verlauf speichern",
"blockBtn": "Anderen Nutzer blockieren", "blockBtn": "Anderen Nutzer blockieren",
"saveBtn": "speichern", "saveBtn": "Speichern",
"displayNameLabel": "Angezeigename", "displayNameLabel": "Angezeigename",
"addressLabel": "Adresse", "addressLabel": "Adresse",
"puzzleGameBtn": "Puzzlespiel", "puzzleGameBtn": "Puzzlespiel",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "en", "@@locale": "en",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -163,7 +166,7 @@
"update": "Update", "update": "Update",
"inviteBtn": "Invite", "inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group", "inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group Name", "groupNameLabel": "Group name",
"viewServerInfo": "Server Info", "viewServerInfo": "Server Info",
"serverSynced": "Synced", "serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityDisconnected": "Server Disconnected",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "es", "@@locale": "es",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "fr", "@@locale": "fr",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Ajouter le contact",
"contactGoto": "Aller à la conversation avec %1",
"settingUIColumnOptionSame": "Même réglage que pour le mode portrait", "settingUIColumnOptionSame": "Même réglage que pour le mode portrait",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -132,7 +135,7 @@
"profileName": "Pseudo", "profileName": "Pseudo",
"editProfileTitle": "Modifier le profil", "editProfileTitle": "Modifier le profil",
"addProfileTitle": "Ajouter un nouveau profil", "addProfileTitle": "Ajouter un nouveau profil",
"deleteBtn": "Supprimer", "deleteBtn": "Effacer",
"unblockBtn": "Débloquer le pair", "unblockBtn": "Débloquer le pair",
"dontSavePeerHistory": "Supprimer l'historique des pairs", "dontSavePeerHistory": "Supprimer l'historique des pairs",
"savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au pair.", "savePeerHistoryDescription": "Détermine s'il faut ou non supprimer tout historique associé au pair.",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "it", "@@locale": "it",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -124,7 +127,7 @@
"noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati", "noPasswordWarning": "Non utilizzare una password su questo account significa che tutti i dati archiviati localmente non verranno criptati",
"radioNoPassword": "Non criptato (senza password)", "radioNoPassword": "Non criptato (senza password)",
"radioUsePassword": "Password", "radioUsePassword": "Password",
"copiedToClipboardNotification": "Copiato negli appunti", "copiedToClipboardNotification": "Copiato negli Appunti",
"copyBtn": "Copia", "copyBtn": "Copia",
"editProfile": "Modifica profilo", "editProfile": "Modifica profilo",
"newProfile": "Nuovo profilo", "newProfile": "Nuovo profilo",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "pl", "@@locale": "pl",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -163,7 +166,7 @@
"update": "Update", "update": "Update",
"inviteBtn": "Invite", "inviteBtn": "Invite",
"inviteToGroupLabel": "Invite to group", "inviteToGroupLabel": "Invite to group",
"groupNameLabel": "Group Name", "groupNameLabel": "Group name",
"viewServerInfo": "Server Info", "viewServerInfo": "Server Info",
"serverSynced": "Synced", "serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityDisconnected": "Server Disconnected",

View File

@ -1,6 +1,9 @@
{ {
"@@locale": "pt", "@@locale": "pt",
"@@last_modified": "2021-07-07T23:42:20+02:00", "@@last_modified": "2021-07-10T17:32:01+02:00",
"addContactConfirm": "Add contact %1",
"addContact": "Add contact",
"contactGoto": "Go to conversation with %1",
"settingUIColumnOptionSame": "Same as portrait mode setting", "settingUIColumnOptionSame": "Same as portrait mode setting",
"settingUIColumnDouble14Ratio": "Double (1:4)", "settingUIColumnDouble14Ratio": "Double (1:4)",
"settingUIColumnDouble12Ratio": "Double (1:2)", "settingUIColumnDouble12Ratio": "Double (1:2)",
@ -163,7 +166,7 @@
"update": "Update", "update": "Update",
"inviteBtn": "Convidar", "inviteBtn": "Convidar",
"inviteToGroupLabel": "Convidar ao grupo", "inviteToGroupLabel": "Convidar ao grupo",
"groupNameLabel": "Nome do Grupo", "groupNameLabel": "Nome do grupo",
"viewServerInfo": "Server Info", "viewServerInfo": "Server Info",
"serverSynced": "Synced", "serverSynced": "Synced",
"serverConnectivityDisconnected": "Server Disconnected", "serverConnectivityDisconnected": "Server Disconnected",

View File

@ -213,8 +213,7 @@ class ProfileInfoState extends ChangeNotifier {
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked", authorization: stringToContactAuthorization(contact["authorization"]),
isInvitation: contact["authorization"] == "unknown",
savePeerHistory: contact["saveConversationHistory"], savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"], numMessages: contact["numMessages"],
numUnread: contact["numUnread"], numUnread: contact["numUnread"],
@ -316,8 +315,7 @@ class ProfileInfoState extends ChangeNotifier {
nickname: contact["name"], nickname: contact["name"],
status: contact["status"], status: contact["status"],
imagePath: contact["picture"], imagePath: contact["picture"],
isBlocked: contact["authorization"] == "blocked", authorization: stringToContactAuthorization(contact["authorization"]),
isInvitation: contact["authorization"] == "unknown",
savePeerHistory: contact["saveConversationHistory"], savePeerHistory: contact["saveConversationHistory"],
numMessages: contact["numMessages"], numMessages: contact["numMessages"],
numUnread: contact["numUnread"], numUnread: contact["numUnread"],
@ -331,13 +329,30 @@ class ProfileInfoState extends ChangeNotifier {
} }
} }
enum ContactAuthorization {
unknown,
approved,
blocked
}
ContactAuthorization stringToContactAuthorization(String authStr) {
switch(authStr) {
case "approved":
return ContactAuthorization.approved;
case "blocked":
return ContactAuthorization.blocked;
default:
return ContactAuthorization.unknown;
}
}
class ContactInfoState extends ChangeNotifier { class ContactInfoState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final String onion; final String onion;
late String _nickname; late String _nickname;
late bool _isInvitation; late ContactAuthorization _authorization;
late bool _isBlocked;
late String _status; late String _status;
late String _imagePath; late String _imagePath;
late String _savePeerHistory; late String _savePeerHistory;
@ -355,8 +370,7 @@ class ContactInfoState extends ChangeNotifier {
this.onion, { this.onion, {
nickname = "", nickname = "",
isGroup = false, isGroup = false,
isInvitation = false, authorization = ContactAuthorization.unknown,
isBlocked = false,
status = "", status = "",
imagePath = "", imagePath = "",
savePeerHistory = "DeleteHistoryConfirmed", savePeerHistory = "DeleteHistoryConfirmed",
@ -367,8 +381,7 @@ class ContactInfoState extends ChangeNotifier {
}) { }) {
this._nickname = nickname; this._nickname = nickname;
this._isGroup = isGroup; this._isGroup = isGroup;
this._isInvitation = isInvitation; this._authorization = authorization;
this._isBlocked = isBlocked;
this._status = status; this._status = status;
this._imagePath = imagePath; this._imagePath = imagePath;
this._totalMessages = numMessages; this._totalMessages = numMessages;
@ -398,15 +411,13 @@ class ContactInfoState extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
bool get isBlocked => this._isBlocked; bool get isBlocked => this._authorization == ContactAuthorization.blocked;
set isBlocked(bool newVal) {
this._isBlocked = newVal;
notifyListeners();
}
bool get isInvitation => this._isInvitation; bool get isInvitation => this._authorization == ContactAuthorization.unknown;
set isInvitation(bool newVal) {
this._isInvitation = newVal; ContactAuthorization get authorization => this._authorization;
set authorization(ContactAuthorization newAuth) {
this._authorization = newAuth;
notifyListeners(); notifyListeners();
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart';
import 'messages/invitemessage.dart'; import 'messages/invitemessage.dart';
import 'messages/malformedmessage.dart'; import 'messages/malformedmessage.dart';
import 'messages/quotedmessage.dart'; import 'messages/quotedmessage.dart';
@ -22,6 +23,8 @@ const TorV3ContactHandleLength = 56;
// Defines the length of a Cwtch v2 Group. // Defines the length of a Cwtch v2 Group.
const GroupConversationHandleLength = 32; const GroupConversationHandleLength = 32;
abstract class Message { abstract class Message {
MessageMetadata getMetadata(); MessageMetadata getMetadata();
Widget getWidget(BuildContext context); Widget getWidget(BuildContext context);

View File

@ -12,6 +12,8 @@ import 'addcontactview.dart';
import '../model.dart'; import '../model.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'messageview.dart';
class ContactsView extends StatefulWidget { class ContactsView extends StatefulWidget {
const ContactsView({Key? key}) : super(key: key); const ContactsView({Key? key}) : super(key: key);
@ -19,6 +21,38 @@ class ContactsView extends StatefulWidget {
_ContactsViewState createState() => _ContactsViewState(); _ContactsViewState createState() => _ContactsViewState();
} }
// selectConversation can be called from anywhere to set the active conversation
void selectConversation(BuildContext context, String handle) {
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
}
void _pushMessageView(BuildContext context, String handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
// assert we have an actual profile...
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder: (context, child) => MessageView(),
);
},
),
);
}
class _ContactsViewState extends State<ContactsView> { class _ContactsViewState extends State<ContactsView> {
late TextEditingController ctrlrFilter; late TextEditingController ctrlrFilter;
bool showSearchBar = false; bool showSearchBar = false;

View File

@ -114,7 +114,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
value: Provider.of<ContactInfoState>(context).isBlocked, value: Provider.of<ContactInfoState>(context).isBlocked,
onChanged: (bool blocked) { onChanged: (bool blocked) {
// Save local blocked status // Save local blocked status
Provider.of<ContactInfoState>(context, listen: false).isBlocked = blocked; Provider.of<ContactInfoState>(context, listen: false).authorization = ContactAuthorization.blocked;
// Save New peer authorization // Save New peer authorization
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion; var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
@ -216,7 +216,7 @@ class _PeerSettingsViewState extends State<PeerSettingsView> {
showAlertDialog(BuildContext context) { showAlertDialog(BuildContext context) {
// set up the buttons // set up the buttons
Widget cancelButton = TextButton( Widget cancelButton = TextButton(
child: Text("Cancel"), child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))), style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); // dismiss dialog Navigator.of(context).pop(); // dismiss dialog

View File

@ -1,3 +1,4 @@
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:cwtch/views/messageview.dart'; import 'package:cwtch/views/messageview.dart';
@ -93,39 +94,12 @@ class _ContactRowState extends State<ContactRow> {
), ),
]), ]),
onTap: () { onTap: () {
setState(() { selectConversation(context, contact.onion);
// requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(contact.onion)!.unreadMessages = 0;
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).selectedConversation = contact.onion;
Provider.of<AppState>(context, listen: false).selectedIndex = null;
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(contact.onion);
});
}, },
)); ));
} }
void _pushMessageView(String handle) {
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
// assert we have an actual profile...
// We need to listen for updates to the profile in order to update things like invitation message bubbles.
var profile = Provider.of<FlwtchState>(builderContext).profs.getProfile(profileOnion)!;
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: profile.contactList.getContact(handle)!),
],
builder: (context, child) => MessageView(),
);
},
),
);
}
void _btnApprove() { void _btnApprove() {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cwtch/models/message.dart'; import 'package:cwtch/models/message.dart';
import 'package:cwtch/views/contactsview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cwtch/widgets/profileimage.dart'; import 'package:cwtch/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -24,6 +25,17 @@ class MessageRowState extends State<MessageRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion; var fromMe = Provider.of<MessageMetadata>(context).senderHandle == Provider.of<ProfileInfoState>(context).onion;
var isContact = Provider.of<ContactListState>(context).getContact(Provider.of<MessageMetadata>(context).senderHandle) != null;
var senderDisplayStr = "";
if (!fromMe) {
ContactInfoState? contact = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageMetadata>(context).senderHandle);
if (contact != null) {
senderDisplayStr = contact.nickname;
} else {
senderDisplayStr = Provider.of<MessageMetadata>(context).senderHandle;
}
}
Widget wdgIcons = Visibility( Widget wdgIcons = Visibility(
visible: this.showMenu, visible: this.showMenu,
@ -45,7 +57,7 @@ class MessageRowState extends State<MessageRow> {
} else { } else {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
Widget wdgPortrait = GestureDetector( Widget wdgPortrait = GestureDetector(
onTap: _btnAdd, onTap: isContact ? _btnGoto : _btnAdd,
child: Padding( child: Padding(
padding: EdgeInsets.all(4.0), padding: EdgeInsets.all(4.0),
child: ProfileImage( child: ProfileImage(
@ -54,6 +66,7 @@ class MessageRowState extends State<MessageRow> {
//maskOut: contact.status != "Authenticated", //maskOut: contact.status != "Authenticated",
border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(), border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor(),
badgeTextColor: Colors.red, badgeColor: Colors.red, badgeTextColor: Colors.red, badgeColor: Colors.red,
tooltip: isContact ? AppLocalizations.of(context)!.contactGoto.replaceFirst("%1", senderDisplayStr) : AppLocalizations.of(context)!.addContact,
))); )));
widgetRow = <Widget>[ widgetRow = <Widget>[
@ -86,25 +99,62 @@ class MessageRowState extends State<MessageRow> {
child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow)))); child: Padding(padding: EdgeInsets.all(2), child: Row(mainAxisAlignment: fromMe ? MainAxisAlignment.end : MainAxisAlignment.start, children: widgetRow))));
} }
void _btnGoto() {
selectConversation(context, Provider.of<MessageMetadata>(context, listen: false).senderHandle);
}
void _btnAdd() { void _btnAdd() {
var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle; var sender = Provider.of<MessageMetadata>(context, listen: false).senderHandle;
if (sender == null || sender == "") { if (sender == null || sender == "") {
print("sender not yet loaded"); print("sender not yet loaded");
return; return;
} }
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion; var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
final setPeerAttribute = {
"EventType": "AddContact",
"Data": {"ImportString": sender},
};
final setPeerAttributeJson = jsonEncode(setPeerAttribute);
Provider.of<FlwtchState>(context, listen: false).cwtch.SendProfileEvent(profileOnion, setPeerAttributeJson);
final snackBar = SnackBar( showAddContactConfirmAlertDialog(context, profileOnion, sender);
content: Text(AppLocalizations.of(context)!.successfullAddedContact), }
duration: Duration(seconds: 2),
showAddContactConfirmAlertDialog(BuildContext context, String profileOnion, String senderOnion) {
// set up the buttons
Widget cancelButton = TextButton(
child: Text(AppLocalizations.of(context)!.cancel),
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
onPressed: () {
Navigator.of(context).pop(); // dismiss dialog
},
);
Widget continueButton = TextButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.addContact),
onPressed: () {
Provider
.of<FlwtchState>(context, listen: false)
.cwtch
.ImportBundle(profileOnion, senderOnion);
final snackBar = SnackBar(
content: Text(AppLocalizations.of(context)!.successfullAddedContact),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Navigator.of(context).pop(); // dismiss dialog
},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text(AppLocalizations.of(context)!.addContactConfirm.replaceFirst("%1", senderOnion)),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
); );
ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
} }

View File

@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
import '../settings.dart'; import '../settings.dart';
class ProfileImage extends StatefulWidget { 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}); ProfileImage({required this.imagePath, required this.diameter, required this.border, this.badgeCount = 0, required this.badgeColor, required this.badgeTextColor, this.maskOut = false, this.tooltip = ""});
final double diameter; final double diameter;
final String imagePath; final String imagePath;
final Color border; final Color border;
@ -13,6 +13,7 @@ class ProfileImage extends StatefulWidget {
final Color badgeColor; final Color badgeColor;
final Color badgeTextColor; final Color badgeTextColor;
final bool maskOut; final bool maskOut;
final String tooltip;
@override @override
_ProfileImageState createState() => _ProfileImageState(); _ProfileImageState createState() => _ProfileImageState();
@ -21,6 +22,21 @@ class ProfileImage extends StatefulWidget {
class _ProfileImageState extends State<ProfileImage> { class _ProfileImageState extends State<ProfileImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var image = Image(
image: AssetImage("assets/" + 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.identifier() == "dark"
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter,
);
return RepaintBoundary( return RepaintBoundary(
child: Stack(children: [ child: Stack(children: [
ClipOval( ClipOval(
@ -33,20 +49,9 @@ class _ProfileImageState extends State<ProfileImage> {
padding: const EdgeInsets.all(2.0), //border size padding: const EdgeInsets.all(2.0), //border size
child: ClipOval( child: ClipOval(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: Image( child: widget.tooltip == "" ? image : Tooltip(
image: AssetImage("assets/" + widget.imagePath), message: widget.tooltip,
filterQuality: FilterQuality.medium, child: image))))),
// 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.identifier() == "dark"
? BlendMode.softLight
: BlendMode.darken
: BlendMode.srcOut,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter,
))))),
Visibility( Visibility(
visible: widget.badgeCount > 0, visible: widget.badgeCount > 0,
child: Positioned( child: Positioned(