diff --git a/android/app/src/main/kotlin/com/example/flutter_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/flutter_app/MainActivity.kt index 6c1c92d..fc1d889 100644 --- a/android/app/src/main/kotlin/com/example/flutter_app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/flutter_app/MainActivity.kt @@ -21,6 +21,8 @@ import cwtch.Cwtch import io.flutter.plugin.common.EventChannel import kotlin.concurrent.thread +import org.json.JSONObject + class MainActivity: FlutterActivity() { // Channel to get app info @@ -71,15 +73,14 @@ class MainActivity: FlutterActivity() { Log.i("MainActivity.kt", "got event chan: " + eventbus_chan + " launching corouting...") GlobalScope.launch(Dispatchers.IO) { while(true) { - //val jsonEvent = Cwtch.getAppBusEvent() - // Log.i("MainActivity.kt", "got appbusEvent: " + jsonEvent) - // launch(Dispatchers.Main) { - // //eventbus_chan.invokeMethod("AppbusEvent", jsonEvent) - //} + val evt = AppbusEvent(Cwtch.getAppBusEvent()) + Log.i("MainActivity.kt", "got appbusEvent: " + evt) + launch(Dispatchers.Main) { + //todo: this elides evt.EventID which may be needed at some point? + eventbus_chan.invokeMethod(evt.EventType, evt.Data) + } } } - - } "SelectProfile" -> { val onion = (call.argument("profile") as? String) ?: ""; @@ -129,4 +130,24 @@ class MainActivity: FlutterActivity() { else -> result.notImplemented() } } + +// source: https://web.archive.org/web/20210203022531/https://stackoverflow.com/questions/41928803/how-to-parse-json-in-kotlin/50468095 +// for reference: +// +// class Response(json: String) : JSONObject(json) { +// val type: String? = this.optString("type") +// val data = this.optJSONArray("data") +// ?.let { 0.until(it.length()).map { i -> it.optJSONObject(i) } } // returns an array of JSONObject +// ?.map { Foo(it.toString()) } // transforms each JSONObject of the array into Foo +// } +// +// class Foo(json: String) : JSONObject(json) { +// val id = this.optInt("id") +// val title: String? = this.optString("title") +// } + class AppbusEvent(json: String) : JSONObject(json) { + val EventType = this.optString("EventType") + val EventID = this.optString("EventID") + val Data = this.optString("Data") + } } diff --git a/android/cwtch/cwtch.aar b/android/cwtch/cwtch.aar index a59fa10..ffdf628 100644 Binary files a/android/cwtch/cwtch.aar and b/android/cwtch/cwtch.aar differ diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 0570d66..c3a701c 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -1,9 +1,12 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:flutter_app/model.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:async'; import 'package:path/path.dart' as path; +import 'package:provider/provider.dart'; import 'cwtch.dart'; @@ -26,14 +29,16 @@ class CwtchGomobile implements Cwtch { Future androidLibraryDir; Future androidHomeDirectory; + ProfileListState profileCN; - CwtchGomobile() { + CwtchGomobile(ProfileListState profs) { print("gomobile.dart: CwtchGomobile()"); + profileCN = profs; androidHomeDirectory = getApplicationDocumentsDirectory(); androidLibraryDir = appInfoPlatform.invokeMethod('getNativeLibDir'); - // final appbusEventChannel = MethodChannel(appbusEventChannelName); - // appbusEventChannel.setMethodCallHandler(this._handleAppbusEvent); + final appbusEventChannel = MethodChannel(appbusEventChannelName); + appbusEventChannel.setMethodCallHandler(this._handleAppbusEvent); } Future Start() async { @@ -46,7 +51,13 @@ class CwtchGomobile implements Cwtch { Future _handleAppbusEvent(MethodCall call) async { final String json = call.arguments; - print("appbus event: ${call.method} $json"); + var obj = jsonDecode(json); + switch (call.method) { + case "NewPeer": + profileCN.add(ProfileInfoState(onion: obj["Identity"], nickname: obj["ProfileName"], imagePath: obj["Path"])); + break; + default: print("unhandled gomobile appbus event: $call"); + } } void SelectProfile(String onion) { diff --git a/lib/main.dart b/lib/main.dart index 2c34e21..548f81a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,4 @@ import 'dart:collection'; -import 'dart:convert'; - import 'package:flutter_app/cwtch/ffi.dart'; import 'package:flutter_app/cwtch/gomobile.dart'; import 'package:flutter/material.dart'; @@ -24,112 +22,70 @@ class Flwtch extends StatefulWidget { class FlwtchState extends State { final TextStyle biggerFont = const TextStyle(fontSize: 18); - - // mergenotes: dan's Cwtch cwtch; bool cwtchInit = false; - - // mergenotes: ui stuff - ProfileModel selectedProfile; + ProfileInfoState selectedProfile; String selectedConversation = ""; var columns = [1]; // default or 'single column' mode //var columns = [1, 1, 2]; - AppModel appStatus; - HashMap profiles; + ProfileListState profs; @override initState() { super.initState(); cwtchInit = false; - - profiles = new HashMap(); - - print("FlwtchState.initState()"); + profs = ProfileListState(); if (Platform.isAndroid) { - cwtch = CwtchGomobile(); + cwtch = CwtchGomobile(profs); } else { cwtch = CwtchFfi(); } + cwtch.Start().then((val) { setState(() { cwtchInit = true; - loadProfiles(); }); }); appStatus = AppModel(cwtch: cwtch); - // Timing issue? Start may not have inited cwtch yet when we ask for getProfiles... } - void loadProfiles() { - cwtch.GetProfiles().then((profilesJson) { - setState(() { - jsonDecode(profilesJson).forEach((profile) { - ProfileModel profile1 = new ProfileModel(); - profile1.onion = profile['onion']; - profile1.nickname = profile['name']; - profile1.creationDate = "4 jan 2020"; - profile1.contacts = new HashMap(); - profile1.imagePath = profile['imagePath']; - - profiles.putIfAbsent(profile1.onion, () => profile1); - }); - }); - }); - } - - ChangeNotifierProvider getOpaqueProvider() { - return ChangeNotifierProvider(create: (context) => OpaqueTheme(Opaque.dark)); - } - - Provider getFlwtchStateProvider() { - return Provider( - create: (_) => this, - ); - } + ChangeNotifierProvider getOpaqueProvider() => ChangeNotifierProvider(create: (context) => OpaqueTheme(Opaque.dark)); + Provider getFlwtchStateProvider() => Provider(create: (_) => this); + ChangeNotifierProvider getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs); @override Widget build(BuildContext context) { appStatus = AppModel(cwtch: cwtch); final newTextTheme = Theme.of(context).textTheme.apply( - bodyColor: Opaque.current().mainTextColor(), - displayColor: Opaque.current().mainTextColor(), - ); + bodyColor: Opaque.current().mainTextColor(), + displayColor: Opaque.current().mainTextColor(), + ); - print("FlwtchState.build() cwtchInit: $cwtchInit"); return MultiProvider( - providers: [getFlwtchStateProvider(), getOpaqueProvider()], - builder: (context, widget) { - return MaterialApp( - title: 'Cwtch', - theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, - primarySwatch: Colors.red, - primaryColor: Provider.of(context) - .current() - .backgroundMainColor(), - canvasColor: Provider.of(context) - .current() - .backgroundPaneColor(), - accentColor: Provider.of(context) - .current() - .defaultButtonColor(), - buttonColor: Provider.of(context) - .current() - .defaultButtonColor(), - textTheme: newTextTheme, - ), - // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(), - // from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(), - home: cwtchInit == true - ? (columns.length == 3 - ? TripleColumnView() - : ProfileMgrView()) - : SplashView(), - );}, + providers: [getFlwtchStateProvider(), getProfileListProvider(), getOpaqueProvider()], + builder: (context, widget) { + return Consumer( + builder: (context, opaque, child) => MaterialApp( + title: 'Cwtch', + theme: ThemeData( + visualDensity: VisualDensity.adaptivePlatformDensity, + primarySwatch: Colors.red, + primaryColor: opaque.current().backgroundMainColor(), + canvasColor: opaque.current().backgroundPaneColor(), + accentColor: opaque.current().defaultButtonColor(), + buttonColor: opaque.current().defaultButtonColor(), + textTheme: newTextTheme, + ), + // from dan: home: cwtchInit == true ? ProfileMgrView(cwtch) : SplashView(), + // from erinn: home: columns.length == 3 ? TripleColumnView() : ProfileMgrView(), + home: cwtchInit == true ? (columns.length == 3 ? TripleColumnView() : ProfileMgrView()) : SplashView(), + ), + ); + }, ); } } diff --git a/lib/model.dart b/lib/model.dart index 6870120..d04d225 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -1,10 +1,12 @@ import 'dart:convert'; import 'dart:ffi'; import 'package:ffi/ffi.dart'; +import 'package:flutter/cupertino.dart'; import 'dart:async'; import 'dart:collection'; import 'cwtch/cwtch.dart'; +import 'main.dart'; //////////////////// /// UI State /// @@ -53,6 +55,116 @@ class ChatMessage { }; } +/////////////////// +/// Providers /// +/////////////////// + +class ProfileListState extends ChangeNotifier { + List _onions = []; + int get num => _onions.length; + + void addAll(Iterable newOnions) { + _onions.addAll(newOnions); + notifyListeners(); + } + + void add(ProfileInfoState newOnion) { + print("ProfileListState: adding " + newOnion.onion +" and notifying"); + _onions.add(newOnion); + notifyListeners(); + } + + List get onions => _onions.sublist(0);//todo: copy?? dont want caller able to bypass changenotifier +} + +class ContactListState extends ChangeNotifier { + List _onions = []; + int get num => _onions.length; + + void addAll(Iterable newOnions) { + _onions.addAll(newOnions); + notifyListeners(); + } + + void add(ContactInfoState newOnion) { + _onions.add(newOnion); + notifyListeners(); + } + + void updateUnreadMessages(String forOnion, int newVal) { + _onions.sort((ContactInfoState a, ContactInfoState b) { return b.unreadMessages - a.unreadMessages; }); + // if(changed) { + notifyListeners(); + //} + } + + List get onions => _onions.sublist(0);//todo: copy?? dont want caller able to bypass changenotifier +} + +class ProfileInfoState extends ChangeNotifier { + final String onion; + String _nickname = ""; + String _imagePath = ""; + int _unreadMessages = 0; + + ProfileInfoState({this.onion, nickname = "", imagePath = "", unreadMessages = 0,}){ + this._nickname = nickname; + this._imagePath = imagePath; + this._unreadMessages = unreadMessages; + } + + String get nickname => this._nickname; + set nickname(String newValue) { + this.nickname = newValue; + notifyListeners(); + } + + String get imagePath => this._imagePath; + set imagePath(String newVal) { + this._imagePath = newVal; + notifyListeners(); + } + + int get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + this._unreadMessages = newVal; + notifyListeners(); + } +} + +class ContactInfoState extends ChangeNotifier { + final String profileOnion; + final String onion; + String _nickname; + bool _isGroup; + bool _isInvitation; + bool _isBlocked; + String _status; + String _imagePath; + int _unreadMessages = 0; + + ContactInfoState({this.profileOnion, this.onion, nickname = "", isGroup = false, isInvitation = false, isBlocked = false, status = "", imagePath = "",}) { + this._nickname = nickname; + this._isGroup = isGroup; + this._isInvitation = isInvitation; + this._isBlocked = isBlocked; + this._status = status; + this._imagePath = imagePath; + } + + get nickname => this._nickname; + set nickname(String newVal) { + this._nickname = newVal; + notifyListeners(); + } + + get unreadMessages => this._unreadMessages; + set unreadMessages(int newVal) { + this._unreadMessages = newVal; + notifyListeners(); + } +} + ///////////// /// ACN /// ///////////// @@ -70,6 +182,7 @@ class AppModel { print(event); yield event; } else { + print("TEST TEST FAIL TEST FAIL 123"); await Future.delayed(Duration(seconds: 1)); } } @@ -81,6 +194,7 @@ class AppModel { if (event != "") { yield event; } else { + print("TOR TEST TEST FAIL TEST FAIL 123"); await Future.delayed(Duration(seconds: 1)); } } diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 00085e9..c8a4317 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -10,7 +10,7 @@ import '../model.dart'; class ContactsView extends StatefulWidget { const ContactsView({Key key, this.profile}) : super(key: key); - final ProfileModel profile; + final ProfileInfoState profile; @override _ContactsViewState createState() => _ContactsViewState(); diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index 4f7f6c0..8436ae4 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -12,7 +12,7 @@ import '../widgets/messagelist.dart'; class MessageView extends StatefulWidget { const MessageView({Key key, this.profile, this.conversationHandle}) : super(key: key); - final ProfileModel profile; + final ProfileInfoState profile; final String conversationHandle; @override diff --git a/lib/views/profilemgrview.dart b/lib/views/profilemgrview.dart index a00f77d..0b85620 100644 --- a/lib/views/profilemgrview.dart +++ b/lib/views/profilemgrview.dart @@ -1,14 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; +import 'package:flutter_app/widgets/profilerow.dart'; import 'package:provider/provider.dart'; import '../main.dart'; import '../model.dart'; -import 'dart:collection'; -import '../opaque.dart'; import 'addeditprofileview.dart'; -import 'contactsview.dart'; -import 'doublecolview.dart'; import 'globalsettingsview.dart'; class ProfileMgrView extends StatefulWidget { @@ -29,7 +24,6 @@ class _ProfileMgrViewState extends State { @override Widget build(BuildContext context) { - return Scaffold ( appBar: AppBar( title: Text('Profiles'), @@ -47,19 +41,6 @@ class _ProfileMgrViewState extends State { ); } - void _pushContactList(ProfileModel profile, bool includeDoublePane) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return Provider( - create: (_) => Provider.of(context), - child: includeDoublePane ? DoubleColumnView() : ContactsView(profile:profile), - ); - }, - ), - ); - } - void _pushGlobalSettings() { Navigator.of(context).push( MaterialPageRoute( @@ -75,14 +56,14 @@ class _ProfileMgrViewState extends State { void _pushAddEditProfile({onion: ""}) { Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return Provider ( - create: (_) => Provider.of(context, listen: false), - child: AddEditProfileView(profileOnion: onion), - ); - }, - ) + MaterialPageRoute( + builder: (BuildContext context) { + return Provider ( + create: (_) => Provider.of(context, listen: false), + child: AddEditProfileView(profileOnion: onion), + ); + }, + ) ); } @@ -110,12 +91,7 @@ class _ProfileMgrViewState extends State { ElevatedButton( child: const Text('Unlock'), onPressed: () { - Provider - .of(context, listen: false) - .cwtch - .LoadProfiles(ctrlrPassword.value.text); - Provider.of(context, listen: false) - .loadProfiles(); + Provider.of(context, listen: false).cwtch.LoadProfiles(ctrlrPassword.value.text); Navigator.pop(context); }, ), @@ -126,42 +102,12 @@ class _ProfileMgrViewState extends State { }); } - String getNick(String profile, String contact) { - return contact == profile ? "me" : Provider.of(context).profiles[profile].contacts[contact].nickname; - } - Widget _buildProfileManager() { - final tiles = Provider.of(context).profiles.values.map( - (ProfileModel profile) { - return ListTile( - leading: SizedBox( - width: 60, - height: 60, - child: ClipOval( - child: SizedBox(width:60, height:60, child:Container(color:Colors.white, width: 60, height: 60, child: Image(image: AssetImage("assets/" + profile.imagePath), width:50,height:50,))), - ), - ) , - trailing: IconButton(icon: Icon(Icons.create, color: Opaque.current().mainTextColor()), onPressed: () { _pushAddEditProfile(onion: profile.onion); }),//(nb: Icons.create is a pencil and we use it for "edit", not create) - title: Text( - profile.nickname, - style: Provider.of(context).biggerFont, - ), - subtitle: Text(profile.onion), - onTap: () { - setState(() { - var flwtch = Provider.of(context, listen:false); - flwtch.cwtch.SelectProfile(profile.onion); - flwtch.setState(() { - flwtch.selectedProfile = profile; - flwtch.selectedConversation = ""; - }); - - switch (flwtch.columns.length) { - case 1: _pushContactList(profile, false); break; - case 2: _pushContactList(profile, true); break; - } // case 3: handled by TripleColumnView - }); - }, + final tiles = Provider.of(context).onions.map( + (ProfileInfoState profile) { + return ChangeNotifierProvider( + create: (context) => profile, + builder: (context, child) => ProfileRow(profile), ); }, ); @@ -173,5 +119,4 @@ class _ProfileMgrViewState extends State { return ListView(children: divided); } -} - +} \ No newline at end of file diff --git a/lib/widgets/messagebubble.dart b/lib/widgets/messagebubble.dart index 0ffc251..230184a 100644 --- a/lib/widgets/messagebubble.dart +++ b/lib/widgets/messagebubble.dart @@ -7,7 +7,7 @@ import '../opaque.dart'; class MessageBubble extends StatefulWidget { MessageBubble({Key key, this.profile, this.contactOnion, this.messageIndex}); - final ProfileModel profile; + final ProfileInfoState profile; final String contactOnion; final int messageIndex; diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index e4a8bed..7354364 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -7,7 +7,7 @@ import '../model.dart'; import 'messagebubble.dart'; class MessageList extends StatefulWidget { - final ProfileModel profile; + final ProfileInfoState profile; final String conversationHandle; const MessageList({Key key, this.profile, this.conversationHandle}) : super(key: key); diff --git a/lib/widgets/profilerow.dart b/lib/widgets/profilerow.dart new file mode 100644 index 0000000..fe49728 --- /dev/null +++ b/lib/widgets/profilerow.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_app/views/addeditprofileview.dart'; +import 'package:flutter_app/views/contactsview.dart'; +import 'package:flutter_app/views/doublecolview.dart'; +import 'package:provider/provider.dart'; + +import '../main.dart'; +import '../model.dart'; +import '../opaque.dart'; + +class ProfileRow extends StatefulWidget { + final ProfileInfoState profile; + ProfileRow(this.profile); + + @override + _ProfileRowState createState() => _ProfileRowState(); +} + +class _ProfileRowState extends State { + @override + Widget build(BuildContext context) { + return ListTile( + leading: SizedBox( + width: 60, + height: 60, + child: ClipOval( + child: SizedBox(width:60, height:60, child:Container(color:Colors.white, width: 60, height: 60, child: Image(image: AssetImage("assets/" + widget.profile.imagePath), width:50,height:50,))), + ), + ) , + trailing: IconButton( + icon: Icon(Icons.create, color: Provider.of(context).current().mainTextColor()), + onPressed: () { _pushAddEditProfile(onion: widget.profile.onion); }, + ),//(nb: Icons.create is a pencil and we use it for "edit", not create) + title: Text( + widget.profile.nickname, + style: Provider.of(context).biggerFont, + ), + subtitle: Text(widget.profile.onion), + onTap: () { + setState(() { + var flwtch = Provider.of(context, listen:false); + flwtch.cwtch.SelectProfile(widget.profile.onion); + flwtch.setState(() { + flwtch.selectedProfile = widget.profile; + flwtch.selectedConversation = ""; + }); + + switch (flwtch.columns.length) { + case 1: _pushContactList(widget.profile, false); break; + case 2: _pushContactList(widget.profile, true); break; + } // case 3: handled by TripleColumnView + }); + }, + ); + } + + void _pushContactList(ProfileInfoState profile, bool includeDoublePane) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return Provider( + create: (_) => Provider.of(context), + child: includeDoublePane ? DoubleColumnView() : ContactsView(profile:profile), + ); + }, + ), + ); + } + + void _pushAddEditProfile({onion: ""}) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return Provider ( + create: (_) => Provider.of(context, listen: false), + child: AddEditProfileView(profileOnion: onion), + ); + }, + ) + ); + } +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 5e7d4e8..dae7277 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:flutter_app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(Flwtch()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);