Merge pull request 'Experimental Check on Group Invitations + Delete Profile Fixes for Passwordless Profiles' (#214) from launch into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #214
This commit is contained in:
Dan Ballard 2021-06-24 12:07:50 -07:00
commit 23b0af77f1
28 changed files with 345 additions and 208 deletions

View File

@ -1 +1 @@
v0.0.2-104-gc1b7e4c-2021-06-22-23-59
v0.0.2-108-g3964348-2021-06-24-17-42

View File

@ -136,7 +136,7 @@ class MainActivity: FlutterActivity() {
super.onResume()
Log.i("MainActivity.kt", "onResume")
if (myReceiver == null) {
Log.i("MainActivity.kt", "onResume registering localbroadcastreceiver")
Log.i("MainActivity.kt", "onResume registering local broadcast receiver / event bus forwarder")
val mc = MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, CWTCH_EVENTBUS)
val filter = IntentFilter("im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS")
myReceiver = MyBroadcastReceiver(mc)
@ -163,7 +163,7 @@ class MainActivity: FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
Log.i("MainActivity.kt", "onDestroy")
Log.i("MainActivity.kt", "onDestroy - cancelling all WORKER_TAG and pruning old work")
WorkManager.getInstance(this).cancelAllWorkByTag(WORKER_TAG)
WorkManager.getInstance(this).pruneWork()
}
@ -188,6 +188,8 @@ class MainActivity: FlutterActivity() {
val Data = this.optString("Data")
}
// MainActivity.MyBroadcastReceiver receives events from the Cwtch service via im.cwtch.flwtch.broadcast.SERVICE_EVENT_BUS Android local broadcast intents
// then it forwards them to the flutter ui engine using the CWTCH_EVENTBUS methodchannel
class MyBroadcastReceiver(mc: MethodChannel) : BroadcastReceiver() {
val eventBus: MethodChannel = mc
@ -198,5 +200,4 @@ class MainActivity: FlutterActivity() {
eventBus.invokeMethod(evtType, evtData)
}
}
}

View File

@ -37,7 +37,8 @@ class CwtchNotifier {
appState.SetAppError(data["Error"]);
break;
case "NewPeer":
profileCN.add(data["Identity"], data["name"], data["picture"], data["ContactsJson"], data["ServerList"], data["Online"] == "true");
// 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");
break;
case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"])?.contactList.add(ContactInfoState(
@ -97,7 +98,9 @@ class CwtchNotifier {
break;
case "NewMessageFromPeer":
notificationManager.notify("New Message From Peer!");
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["RemotePeer"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.unreadMessages++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["RemotePeer"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["RemotePeer"], DateTime.now());
break;
@ -122,7 +125,9 @@ class CwtchNotifier {
case "NewMessageFromGroup":
if (data["ProfileOnion"] != data["RemotePeer"]) {
//not from me
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
if (appState.selectedProfile != data["ProfileOnion"] || appState.selectedConversation != data["GroupID"]) {
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.unreadMessages++;
}
profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(data["GroupID"])!.totalMessages++;
profileCN.getProfile(data["ProfileOnion"])?.contactList.updateLastMessageTime(data["GroupID"], DateTime.now());
} else {

View File

@ -1,6 +1,7 @@
{
"@@locale": "de",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -1,6 +1,7 @@
{
"@@locale": "en",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -1,6 +1,7 @@
{
"@@locale": "es",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -1,6 +1,7 @@
{
"@@locale": "fr",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -1,6 +1,7 @@
{
"@@locale": "it",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -1,6 +1,7 @@
{
"@@locale": "pt",
"@@last_modified": "2021-06-16T23:15:48+02:00",
"@@last_modified": "2021-06-24T18:24:27+02:00",
"groupInviteSettingsWarning": "You have been invited to join a group! Please enable the Group Chat Experiment in Settings to view this Invitation.",
"shutdownCwtchAction": "Shutdown Cwtch",
"shutdownCwtchDialog": "Are you sure you want to shutdown Cwtch? This will close all connections, and exit the application.",
"shutdownCwtchDialogTitle": "Shutdown Cwtch?",

View File

@ -45,10 +45,6 @@ class Flwtch extends StatefulWidget {
class FlwtchState extends State<Flwtch> {
final TextStyle biggerFont = const TextStyle(fontSize: 18);
late Cwtch cwtch;
late ProfileInfoState selectedProfile;
String selectedConversation = "";
var columns = [1]; // default or 'single column' mode
//var columns = [1, 1, 2];
late ProfileListState profs;
final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler');
final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdown');
@ -108,7 +104,7 @@ class FlwtchState extends State<Flwtch> {
supportedLocales: AppLocalizations.supportedLocales,
title: 'Cwtch',
theme: mkThemeData(settings),
home: appState.cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ShiftRightFixer(child: ProfileMgrView())) : SplashView(),
home: appState.cwtchInit == true ? ShiftRightFixer(child: ProfileMgrView()) : SplashView(),
),
);
},
@ -128,24 +124,38 @@ class FlwtchState extends State<Flwtch> {
});
}
// Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt
// coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
Future<void> _externalNotificationClicked(MethodCall call) async {
var args = jsonDecode(call.arguments);
var profile = profs.getProfile(args["ProfileOnion"])!;
var contact = profile.contactList.getContact(args["RemotePeer"])!;
contact.unreadMessages = 0;
navKey.currentState?.push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: contact),
],
builder: (context, child) => MessageView(),
);
},
),
);
// single pane mode pushes; double pane mode reads AppState.selectedProfile/Conversation
var isLandscape = Provider.of<AppState>(navKey.currentContext!, listen: false).isLandscape(navKey.currentContext!);
if (Provider.of<Settings>(navKey.currentContext!, listen: false).uiColumns(isLandscape).length == 1) {
if (navKey.currentContext?.findAncestorWidgetOfExactType<MessageView>() != null) {
print("messageview already open; popping before pushing replacement");
navKey.currentState?.pop();
}
navKey.currentState?.push(
MaterialPageRoute<void>(
builder: (BuildContext builderContext) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: profile),
ChangeNotifierProvider.value(value: contact),
],
builder: (context, child) => MessageView(),
);
},
),
);
} else { //dual pane
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = args["ProfileOnion"];
Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = args["RemotePeer"];
}
}
@override

View File

@ -13,7 +13,6 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:glob/glob.dart';
import 'package:glob/list_local_fs.dart';
var globalSettings = Settings(Locale("en", ''), OpaqueDark());
var globalErrorHandler = ErrorHandler();

View File

@ -38,10 +38,10 @@ class ProfileListState extends ChangeNotifier {
List<ProfileInfoState> _profiles = [];
int get num => _profiles.length;
void add(String onion, String name, String picture, String contactsJson, String serverJson, bool online) {
void add(String onion, String name, String picture, 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));
_profiles.add(ProfileInfoState(onion: onion, nickname: name, imagePath: picture, contactsJson: contactsJson, serversJson: serverJson, online: online, encrypted: encrypted));
} else {
_profiles[idx].updateFrom(onion, name, picture, contactsJson, serverJson, online);
}
@ -64,6 +64,8 @@ class ProfileListState extends ChangeNotifier {
class AppState extends ChangeNotifier {
bool cwtchInit = false;
String appError = "";
String? _selectedProfile;
String? _selectedConversation;
void SetCwtchInit() {
cwtchInit = true;
@ -74,6 +76,20 @@ class AppState extends ChangeNotifier {
appError = error;
notifyListeners();
}
String? get selectedProfile => _selectedProfile;
set selectedProfile(String? newVal) {
this._selectedProfile = newVal;
notifyListeners();
}
String? get selectedConversation => _selectedConversation;
set selectedConversation(String? newVal) {
this._selectedConversation = newVal;
notifyListeners();
}
bool isLandscape(BuildContext c) => MediaQuery.of(c).size.width > MediaQuery.of(c).size.height;
}
class ContactListState extends ChangeNotifier {
@ -161,6 +177,10 @@ class ProfileInfoState extends ChangeNotifier {
int _unreadMessages = 0;
bool _online = false;
// assume profiles are encrypted...this will be set to false
// in the constructor if the profile is encrypted with the defacto password.
bool _encrypted = true;
ProfileInfoState({
required this.onion,
nickname = "",
@ -169,11 +189,13 @@ class ProfileInfoState extends ChangeNotifier {
contactsJson = "",
serversJson = "",
online = false,
encrypted = true,
}) {
this._nickname = nickname;
this._imagePath = imagePath;
this._unreadMessages = unreadMessages;
this._online = online;
this._encrypted = encrypted;
if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
List<dynamic> contacts = jsonDecode(contactsJson);
@ -227,6 +249,9 @@ class ProfileInfoState extends ChangeNotifier {
notifyListeners();
}
// Check encrypted status for profile info screen
bool get isEncrypted => this._encrypted;
String get nickname => this._nickname;
set nickname(String newValue) {
this._nickname = newValue;
@ -491,6 +516,8 @@ class MessageState extends ChangeNotifier {
}
set loaded(bool newVal) {
// quickly-arriving messages get discarded before loading sometimes
if (!hasListeners) return;
this._loaded = newVal;
notifyListeners();
}

View File

@ -9,6 +9,13 @@ import 'opaque.dart';
const TapirGroupsExperiment = "tapir-groups-experiment";
enum DualpaneMode {
Single,
Dual1to2,
Dual1to4,
CopyPortrait,
}
/// Settings govern the *Globally* relevant settings like Locale, Theme and Experiments.
/// We also provide access to the version information here as it is also accessed from the
/// Settings Pane.
@ -19,6 +26,8 @@ class Settings extends ChangeNotifier {
// explicitly set experiments to false until told otherwise...
bool experimentsEnabled = false;
HashMap<String, bool> experiments = HashMap.identity();
DualpaneMode _uiColumnModePortrait = DualpaneMode.Single;
DualpaneMode _uiColumnModeLandscape = DualpaneMode.CopyPortrait;
bool blockUnknownConnections = false;
@ -73,6 +82,10 @@ class Settings extends ChangeNotifier {
// Set the internal experiments map. Casting from the Map<dynamic, dynamic> that we get from JSON
experiments = new HashMap<String, bool>.from(settings["Experiments"]);
// single pane vs dual pane preferences
_uiColumnModePortrait = uiColumnModeFromString(settings["UIColumnModePortrait"]);
_uiColumnModeLandscape = uiColumnModeFromString(settings["UIColumnModeLandscape"]);
// Push the experimental settings to Consumers of Settings
notifyListeners();
}
@ -134,6 +147,55 @@ class Settings extends ChangeNotifier {
notifyListeners();
}
DualpaneMode get uiColumnModePortrait => _uiColumnModePortrait;
set uiColumnModePortrait(DualpaneMode newval) {
this._uiColumnModePortrait = newval;
notifyListeners();
}
DualpaneMode get uiColumnModeLandscape => _uiColumnModeLandscape;
set uiColumnModeLandscape(DualpaneMode newval) {
this._uiColumnModeLandscape = newval;
notifyListeners();
}
List<int> uiColumns(bool isLandscape) {
var m = (!isLandscape || uiColumnModeLandscape == DualpaneMode.CopyPortrait) ? uiColumnModePortrait : uiColumnModeLandscape;
switch(m) {
case DualpaneMode.Single: return [1];
case DualpaneMode.Dual1to2: return [1, 2];
case DualpaneMode.Dual1to4: return [1, 4];
}
print("impossible column configuration: portrait/$uiColumnModePortrait landscape/$uiColumnModeLandscape");
return [1];
}
static List<DualpaneMode> uiColumnModeOptions(bool isLandscape) {
if (isLandscape) return [DualpaneMode.CopyPortrait, DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4,];
else return [DualpaneMode.Single, DualpaneMode.Dual1to2, DualpaneMode.Dual1to4];
}
static DualpaneMode uiColumnModeFromString(String m) {
switch(m) {
case "DualpaneMode.Single": return DualpaneMode.Single;
case "DualpaneMode.Dual1to2": return DualpaneMode.Dual1to2;
case "DualpaneMode.Dual1to4": return DualpaneMode.Dual1to4;
case "DualpaneMode.CopyPortrait": return DualpaneMode.CopyPortrait;
}
print("Error: ui requested translation of column mode [$m] which doesn't exist");
return DualpaneMode.Single;
}
static String uiColumnModeToString(DualpaneMode m) {
// todo: translate
switch(m) {
case DualpaneMode.Single: return "Single";
case DualpaneMode.Dual1to2: return "Double (1:2)";
case DualpaneMode.Dual1to4: return "Double (1:4)";
case DualpaneMode.CopyPortrait: return "Same as portrait mode setting";
}
}
/// Construct a default settings object.
Settings(this.locale, this.theme);
@ -150,7 +212,9 @@ class Settings extends ChangeNotifier {
"ExperimentsEnabled": this.experimentsEnabled,
"Experiments": experiments,
"StateRootPane": 0,
"FirstTime": false
"FirstTime": false,
"UIColumnModePortrait": uiColumnModePortrait.toString(),
"UIColumnModeLandscape": uiColumnModeLandscape.toString(),
};
}
}

View File

@ -155,7 +155,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
visible: usePassword,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[
Visibility(
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty,
visible: Provider.of<ProfileInfoState>(context, listen: false).onion.isNotEmpty && Provider.of<ProfileInfoState>(context).isEncrypted,
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
CwtchLabel(label: AppLocalizations.of(context)!.currentPasswordLabel),
SizedBox(
@ -165,7 +165,7 @@ class _AddEditProfileViewState extends State<AddEditProfileView> {
controller: ctrlrOldPass,
validator: (value) {
// Password field can be empty when just updating the profile, not on creation
if (Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
if (Provider.of<ProfileInfoState>(context).isEncrypted && Provider.of<ProfileInfoState>(context, listen: false).onion.isEmpty && value.isEmpty && usePassword) {
return AppLocalizations.of(context)!.passwordErrorEmpty;
}
if (Provider.of<ErrorHandler>(context).deleteProfileError == true) {

View File

@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'contactsview.dart';
import 'messageview.dart';
@ -14,24 +15,25 @@ class DoubleColumnView extends StatefulWidget {
class _DoubleColumnViewState extends State<DoubleColumnView> {
@override
Widget build(BuildContext context) {
var flwtch = Provider.of<FlwtchState>(context);
var flwtch = Provider.of<AppState>(context);
var cols = Provider.of<Settings>(context).uiColumns(true);
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
flex: flwtch.columns[0],
flex: cols[0],
child: ContactsView(
key: widget.key,
),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedConversation == ""
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
flex: cols[1],
child: flwtch.selectedConversation == null
? Card(child:Center(child: Text(AppLocalizations.of(context)!.addContactFirst)))
: //dev
MultiProvider(providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context)),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation)!),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(flwtch.selectedConversation!)!),
], child: Container(child: MessageView())),
),
],

View File

@ -80,24 +80,33 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
secondary: Icon(CwtchIcons.change_theme, color: settings.current().mainTextColor()),
),
ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns", style: TextStyle(color: settings.current().mainTextColor())),
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Portrait Mode", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: "Single",
value: settings.uiColumnModePortrait.toString(),
onChanged: (String? newValue) {
if (newValue == "Double (1:2)") {
Provider.of<FlwtchState>(context).columns = [1, 2];
} else if (newValue == "Double (1:4)") {
Provider.of<FlwtchState>(context).columns = [1, 4];
} else {
Provider.of<FlwtchState>(context).columns = [1];
}
settings.uiColumnModePortrait = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
// TODO: Only allow in landscape?
items: (Platform.isAndroid ? ["Single"] : ["Single", "Double (1:2)", "Double (1:4)"]).map<DropdownMenuItem<String>>((String value) {
items: Settings.uiColumnModeOptions(false).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value)),
);
}).toList())),
ListTile(
title: Text(/*AppLocalizations.of(context)!.settingLanguage*/ "UI Columns in Landscape Mode", style: TextStyle(color: settings.current().mainTextColor())),
leading: Icon(Icons.table_chart, color: settings.current().mainTextColor()),
trailing: DropdownButton(
value: settings.uiColumnModeLandscape.toString(),
onChanged: (String? newValue) {
settings.uiColumnModeLandscape = Settings.uiColumnModeFromString(newValue!);
saveSettings(context);
},
items: Settings.uiColumnModeOptions(true).map<DropdownMenuItem<String>>((DualpaneMode value) {
return DropdownMenuItem<String>(
value: value.toString(),
child: Text(Settings.uiColumnModeToString(value)),
);
}).toList())),
SwitchListTile(

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:cwtch/cwtch_icons_icons.dart';
import 'package:cwtch/widgets/profileimage.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cwtch/views/peersettingsview.dart';
@ -42,11 +43,24 @@ class _MessageViewState extends State<MessageView> {
@override
Widget build(BuildContext context) {
var appState = Provider.of<AppState>(context);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text(Provider.of<ContactInfoState>(context).nickname),
// setting leading to null makes it do the default behaviour; container() hides it
leading: Provider.of<Settings>(context).uiColumns(appState.isLandscape(context)).length > 1 ? Container() : null,
title: Row(children: [
ProfileImage(
imagePath: Provider.of<ContactInfoState>(context).imagePath,
diameter: 42,
border: Provider.of<Settings>(context).current().portraitOnlineBorderColor(),
badgeTextColor: Colors.red,
badgeColor: Colors.red,
),
SizedBox(
width: 10,
),Text(Provider.of<ContactInfoState>(context).nickname)]),
actions: [
//IconButton(icon: Icon(Icons.chat), onPressed: _pushContactSettings),
//IconButton(icon: Icon(Icons.list), onPressed: _pushContactSettings),

View File

@ -42,41 +42,45 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
// Prevents Android back button from closing the app on the profile manager screen
// (which would shutdown connections and all kinds of other expensive to generate things)
// TODO pop up a dialogue regarding closing the app?
builder: (context, settings, child) => WillPopScope(
onWillPop: () async {
_showShutdown();
return closeApp;
},
child: Scaffold(
backgroundColor: settings.theme.backgroundMainColor(),
appBar: AppBar(
title: Row(children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 32,
height: 32,
colorBlendMode: BlendMode.dstIn,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
builder: (context, settings, child) =>
WillPopScope(
onWillPop: () async {
_showShutdown();
return closeApp;
},
child: Scaffold(
backgroundColor: settings.theme.backgroundMainColor(),
appBar: AppBar(
title: Row(children: [
Image(
image: AssetImage("assets/core/knott-white.png"),
filterQuality: FilterQuality.medium,
isAntiAlias: true,
width: 32,
height: 32,
colorBlendMode: BlendMode.dstIn,
color: Provider
.of<Settings>(context)
.theme
.backgroundHilightElementColor(),
),
SizedBox(
width: 10,
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
]),
actions: getActions(),
),
SizedBox(
width: 10,
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
Expanded(child: Text(AppLocalizations.of(context)!.titleManageProfiles, style: TextStyle(color: settings.current().mainTextColor())))
]),
actions: getActions(),
),
floatingActionButton: FloatingActionButton(
onPressed: _pushAddEditProfile,
tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
child: Icon(
Icons.add,
semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn,
),
),
body: _buildProfileManager(),
)),
body: _buildProfileManager(),
)),
);
}

View File

@ -3,9 +3,12 @@ import 'package:cwtch/views/profilemgrview.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../model.dart';
import '../settings.dart';
import 'contactsview.dart';
import 'messageview.dart';
// currently unused but maybe one day?
class TripleColumnView extends StatefulWidget {
@override
_TripleColumnViewState createState() => _TripleColumnViewState();
@ -14,19 +17,22 @@ class TripleColumnView extends StatefulWidget {
class _TripleColumnViewState extends State<TripleColumnView> {
@override
Widget build(BuildContext context) {
var flwtch = Provider.of<FlwtchState>(context);
var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context);
var columns = settings.uiColumns(appState.isLandscape(context));
return Flex(direction: Axis.horizontal, children: <Widget>[
Flexible(
flex: flwtch.columns[0],
flex: columns[0],
child: ProfileMgrView(),
),
Flexible(
flex: flwtch.columns[1],
child: flwtch.selectedProfile == null ? Center(child: Text(AppLocalizations.of(context)!.createProfileToBegin)) : ContactsView(), //dev
flex: columns[1],
child: appState.selectedProfile == null ? Center(child: Text(AppLocalizations.of(context)!.createProfileToBegin)) : ContactsView(), //dev
),
Flexible(
flex: flwtch.columns[2],
child: flwtch.selectedConversation == ""
flex: columns[2],
child: appState.selectedConversation == null
? Center(child: Text(AppLocalizations.of(context)!.addContactFirst))
: //dev
Container(child: MessageView()),

View File

@ -20,6 +20,8 @@ class _ContactRowState extends State<ContactRow> {
var contact = Provider.of<ContactInfoState>(context);
return Card(
clipBehavior: Clip.antiAlias,
color: Provider.of<AppState>(context).selectedConversation == contact.onion ? Provider.of<Settings>(context).theme.backgroundHilightElementColor() : null,
borderOnForeground: false,
child: InkWell(
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding(
@ -80,17 +82,19 @@ class _ContactRowState extends State<ContactRow> {
]),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.setState(() => flwtch.selectedConversation = contact.onion);
// case 2/3 handled by Double/TripleColumnView respectively
if (flwtch.columns.length == 1) _pushMessageView(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;
// 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) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages = 0;
var profileOnion = Provider.of<ProfileInfoState>(context, listen: false).onion;
Navigator.of(context).push(
MaterialPageRoute<void>(

View File

@ -35,6 +35,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
isAccepted = Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget) != null;
var prettyDate = "";
var borderRadiousEh = 15.0;
var showGroupInvite = Provider.of<Settings>(context).isExperimentEnabled(TapirGroupsExperiment);
rejected = Provider.of<MessageState>(context).flags & 0x01 == 0x01;
var myKey = Provider.of<MessageState>(context).profileOnion + "::" + Provider.of<MessageState>(context).contactHandle + "::" + Provider.of<MessageState>(context).messageIndex.toString();
@ -66,14 +67,18 @@ class InvitationBubbleState extends State<InvitationBubble> {
return MalformedBubble();
}
var wdgMessage = fromMe
var wdgMessage = isGroup && !showGroupInvite ?
Text(AppLocalizations.of(context)!.groupInviteSettingsWarning) :
fromMe
? senderInviteChrome(AppLocalizations.of(context)!.sendAnInvitation,
isGroup ? Provider.of<ProfileInfoState>(context).contactList.getContact(Provider.of<MessageState>(context).inviteTarget)!.nickname : Provider.of<MessageState>(context).message, myKey)
: (inviteChrome(isGroup ? AppLocalizations.of(context)!.inviteToGroup : AppLocalizations.of(context)!.contactSuggestion, Provider.of<MessageState>(context).inviteNick,
Provider.of<MessageState>(context).inviteTarget, myKey));
Widget wdgDecorations;
if (fromMe) {
if (isGroup && !showGroupInvite) {
wdgDecorations = Text('\u202F');
} else if (fromMe) {
wdgDecorations = MessageBubbleDecoration(ackd: Provider.of<MessageState>(context).ackd, errored: Provider.of<MessageState>(context).error, fromMe: fromMe, prettyDate: prettyDate);
} else if (isAccepted) {
wdgDecorations = Text(AppLocalizations.of(context)!.accepted + '\u202F');
@ -109,7 +114,7 @@ class InvitationBubbleState extends State<InvitationBubble> {
child: Padding(
padding: EdgeInsets.all(9.0),
child: Wrap(runAlignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly, runSpacing: 1.0, crossAxisAlignment: WrapCrossAlignment.center, children: [
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(CwtchIcons.send_invite, size: 32))),
Center(widthFactor: 1, child: Padding(padding: EdgeInsets.all(10.0), child: Icon(isGroup && !showGroupInvite ? CwtchIcons.enable_experiments : CwtchIcons.send_invite, size: 32))),
Center(
widthFactor: 1.0,
child: Column(

View File

@ -30,84 +30,96 @@ class _ProfileRowState extends State<ProfileRow> {
padding: const EdgeInsets.all(2.0), //border size
child: ProfileImage(
badgeCount: 0,
badgeColor: Provider.of<Settings>(context).theme.portraitProfileBadgeColor(),
badgeTextColor: Provider.of<Settings>(context).theme.portraitProfileBadgeTextColor(),
badgeColor: Provider
.of<Settings>(context)
.theme
.portraitProfileBadgeColor(),
badgeTextColor: Provider
.of<Settings>(context)
.theme
.portraitProfileBadgeTextColor(),
diameter: 64.0,
imagePath: profile.imagePath,
border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())),
border: profile.isOnline ? Provider
.of<Settings>(context)
.theme
.portraitOnlineBorderColor() : Provider
.of<Settings>(context)
.theme
.portraitOfflineBorderColor())),
Expanded(
child: Column(
children: [
Text(
profile.nickname,
semanticsLabel: profile.nickname,
style: Provider.of<FlwtchState>(context).biggerFont,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
],
)),
children: [
Text(
profile.nickname,
semanticsLabel: profile.nickname,
style: Provider
.of<FlwtchState>(context)
.biggerFont,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
],
)),
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context)!.editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath);
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath, encrypted: profile.isEncrypted);
},
)
],
),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.setState(() {
flwtch.selectedProfile = profile;
flwtch.selectedConversation = "";
});
var appState = Provider.of<AppState>(context, listen: false);
appState.selectedProfile = profile.onion;
appState.selectedConversation = null;
switch (flwtch.columns.length) {
case 1:
_pushContactList(profile, false);
break;
case 2:
_pushContactList(profile, true);
break;
} // case 3: handled by TripleColumnView
_pushContactList(profile, appState.isLandscape(context));//orientation == Orientation.landscape);
});
},
));
}
void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {
void _pushContactList(ProfileInfoState profile, bool isLandscape) {
Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(name: "conversations"),
builder: (BuildContext buildcontext) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (context, widget) => includeDoublePane ? DoubleColumnView() : ContactsView(),
);
return OrientationBuilder(
builder: (orientationBuilderContext, orientation) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
],
builder: (innercontext, widget) {
var appState = Provider.of<AppState>(context);
var settings = Provider.of<Settings>(context);
return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : ContactsView();
}
);
});
},
),
);
}
void _pushAddEditProfile({onion: "", displayName: "", profileImage: ""}) {
void _pushAddEditProfile({onion: "", displayName: "", profileImage: "", encrypted: true}) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<ProfileInfoState>(
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage),
create: (_) => ProfileInfoState(onion: onion, nickname: displayName, imagePath: profileImage, encrypted: encrypted),
),
],
builder: (context, widget) => AddEditProfileView(),

View File

@ -6,10 +6,6 @@
#include "generated_plugin_registrant.h"
#include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) window_size_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowSizePlugin");
window_size_plugin_register_with_registrar(window_size_registrar);
}

View File

@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
window_size
)
set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -1,13 +1,13 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
ansicolor:
dependency: transitive
description:
name: archive
name: ansicolor
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
version: "2.0.1"
args:
dependency: transitive
description:
@ -42,7 +42,7 @@ packages:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.1"
clock:
dependency: transitive
description:
@ -57,13 +57,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
@ -111,11 +104,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@ -131,11 +119,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: "direct main"
description:
@ -157,13 +140,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
integration_test:
dependency: "direct main"
injector:
dependency: transitive
description:
name: integration_test
name: injector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+3"
version: "2.0.0"
intl:
dependency: transitive
description:
@ -192,6 +175,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
msix:
dependency: "direct dev"
description:
name: msix
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
nested:
dependency: transitive
description:
@ -213,6 +203,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
package_info_plus:
dependency: "direct main"
description:
@ -372,13 +369,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
sync_http:
dependency: transitive
description:
name: sync_http
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
term_glyph:
dependency: transitive
description:
@ -392,7 +382,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
version: "0.4.1"
typed_data:
dependency: transitive
description:
@ -407,20 +397,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
webdriver:
dependency: transitive
description:
name: webdriver
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
win32:
dependency: transitive
description:
@ -442,6 +418,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.2"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
sdks:
dart: ">=2.13.0 <3.0.0"
flutter: ">=1.20.0"

View File

@ -37,13 +37,8 @@ dependencies:
desktop_notifications: 0.5.0
glob: any
# todo: flutter_driver causes version conflict. eg https://github.com/flutter/flutter/issues/44829
# testing-related deps
integration_test: ^1.0.0
flutter_test:
sdk: flutter
flutter_driver:
sdk: flutter
dev_dependencies:
msix: ^2.1.3

View File

@ -6,9 +6,6 @@
#include "generated_plugin_registrant.h"
#include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
WindowSizePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowSizePlugin"));
}

View File

@ -3,7 +3,6 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
window_size
)
set(PLUGIN_BUNDLED_LIBRARIES)