diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index eeac635b..e6cb9215 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2023-09-26-13-15-v0.0.10 \ No newline at end of file +2024-01-03-20-52-v0.0.10-4-g6c0b2e2 \ No newline at end of file diff --git a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt index d2eafd73..7d19a003 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -331,8 +331,9 @@ class MainActivity: FlutterActivity() { val conversation: Int = call.argument("conversation") ?: 0 val indexI: Int = call.argument("index") ?: 0 val count: Int = call.argument("count") ?: 1 + val ucount : Int = maxOf(1, count) // don't allow negative counts - result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong())) + result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), ucount.toLong())) return } "SendMessage" -> { diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 1cc06e22..b19d0f72 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -156,4 +156,6 @@ abstract class Cwtch { void DeleteServerInfo(String profile, String handle); void PublishServerUpdate(String onion); Future ConfigureConnections(String onion, bool listen, bool peers, bool servers); + + bool IsLoaded(); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index aeb81df9..059ff55d 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -268,6 +268,8 @@ class CwtchFfi implements Cwtch { // Called on object being disposed to (presumably on app close) to close the isolate that's listening to libcwtch-go events @override void dispose() { + EnvironmentConfig.debugLog("tearing down cwtch FFI isolate"); + library.close(); cwtchIsolate.kill(priority: Isolate.immediate); } @@ -1121,4 +1123,11 @@ class CwtchFfi implements Cwtch { PublishServerUpdate(utf8profile, utf8profile.length); malloc.free(utf8profile); } + + @override + bool IsLoaded() { + bool check = library.providesSymbol("c_UpdateSettings"); + EnvironmentConfig.debugLog("Checking that the FFI Interface is Correctly Loaded... $check"); + return check; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 4e9804f0..3eca7ecd 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -469,4 +469,9 @@ class CwtchGomobile implements Cwtch { void PublishServerUpdate(String profile) { cwtchPlatform.invokeMethod("PublishServerUpdate", {"ProfileOnion": profile}); } + + @override + bool IsLoaded() { + return true; + } } diff --git a/lib/main.dart b/lib/main.dart index d58ef0b7..45dd4b64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -173,6 +173,10 @@ class FlwtchState extends State with WindowListener { getServerListStateProvider(), ], builder: (context, widget) { + // in test mode...rebuild everything every second...if cwtch isn't loaded... + if (EnvironmentConfig.TEST_MODE && cwtch.IsLoaded() == false) { + Timer t = new Timer.periodic(Duration(seconds: 1), (Timer t) => setState(() {})); + } return Consumer2( builder: (context, settings, appState, child) => MaterialApp( key: Key('app'), @@ -190,7 +194,7 @@ class FlwtchState extends State with WindowListener { title: 'Cwtch', showSemanticsDebugger: settings.useSemanticDebugger, theme: mkThemeData(settings), - home: (!appState.cwtchInit || appState.modalState != ModalState.none) ? SplashView() : ProfileMgrView(), + home: (!appState.cwtchInit || appState.modalState != ModalState.none) || !cwtch.IsLoaded() ? SplashView() : ProfileMgrView(), ), ); }, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index e0931169..2dfae7f2 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -75,6 +75,8 @@ class ContactInfoState extends ChangeNotifier { DateTime _lastRetryTime = DateTime.now(); DateTime loaded = DateTime.now(); + List contactEvents = List.empty(growable: true); + ContactInfoState( this.profileOnion, this.identifier, @@ -198,6 +200,7 @@ class ContactInfoState extends ChangeNotifier { set status(String newVal) { this._status = newVal; + this.contactEvents.add(ContactEvent("Update Peer Status Received: $newVal")); notifyListeners(); } @@ -486,3 +489,11 @@ class ContactInfoState extends ChangeNotifier { } } } + +class ContactEvent { + String summary; + late DateTime timestamp; + ContactEvent(this.summary) { + this.timestamp = DateTime.now(); + } +} diff --git a/lib/models/contactlist.dart b/lib/models/contactlist.dart index ef8a0cf4..e8709508 100644 --- a/lib/models/contactlist.dart +++ b/lib/models/contactlist.dart @@ -98,7 +98,7 @@ class ContactListState extends ChangeNotifier { notifyListeners(); //} } - + void updateLastMessageReceivedTime(int forIdentifier, DateTime newMessageTime) { var contact = getContact(forIdentifier); if (contact == null) return; diff --git a/lib/models/messagecache.dart b/lib/models/messagecache.dart index b1807a0a..94bb834e 100644 --- a/lib/models/messagecache.dart +++ b/lib/models/messagecache.dart @@ -30,22 +30,27 @@ class LocalIndexMessage { this.messageId = messageId; this.cacheOnly = cacheOnly; this.isLoading = isLoading; - if (isLoading) { - loader = Completer(); - loaded = loader.future; + loader = Completer(); + loaded = loader.future; + if (!isLoading) { + loader.complete(); // complete this } } void finishLoad(int messageId) { this.messageId = messageId; - isLoading = false; - loader.complete(true); + if (!loader.isCompleted) { + isLoading = false; + loader.complete(true); + } } void failLoad() { this.messageId = null; - isLoading = false; - loader.complete(true); + if (!loader.isCompleted) { + isLoading = false; + loader.complete(true); + } } Future waitForLoad() { @@ -95,7 +100,7 @@ class MessageCache extends ChangeNotifier { this._storageMessageCount = newval; } - // On android reconnect, if backend supplied message count > UI message count, add the differnce to the front of the index + // On android reconnect, if backend supplied message count > UI message count, add the difference to the front of the index void addFrontIndexGap(int count) { this._indexUnsynced = count; } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 3269f25a..a1e62ac3 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:cwtch/config.dart'; import 'package:cwtch/models/remoteserver.dart'; @@ -248,8 +249,22 @@ class ProfileInfoState extends ChangeNotifier { if (profileContact != null) { profileContact.status = contact["status"]; - var newCount = contact["numMessages"]; + var newCount = contact["numMessages"] as int; if (newCount != profileContact.totalMessages) { + if (newCount < profileContact.totalMessages) { + // on Android, when sharing a file the UI may be briefly unloaded for the + // OS to display the file management/selection screen. Afterwards a + // call to ReconnectCwtchForeground will be made which will refresh all values (including count of numMessages) + // **at the same time** the foreground will increment .totalMessages and send a new message to the backend. + // This will result in a negative number of messages being calculated here, and an incorrect totalMessage count. + // This bug is exacerbated in debug mode, and when multiple files are sent in succession. Both cases result in multiple ReconnectCwtchForeground + // events that have the potential to conflict with currentMessageCounts. + // Note that *if* a new message came in at the same time, we would be unable to distinguish this case - as such this is specific instance of a more general problem + // TODO: A true-fix to this bug is to implement a syncing step in the foreground where totalMessages and inFlightMessages can be distinguished + // This requires a change to the backend to confirm submission of an inFlightMessage, which will be implemented in #664 + EnvironmentConfig.debugLog("Conflicting message counts: $newCount ${profileContact.totalMessages}"); + newCount = max(newCount, profileContact.totalMessages); + } profileContact.messageCache.addFrontIndexGap(newCount - profileContact.totalMessages); } profileContact.totalMessages = newCount; diff --git a/lib/models/redaction.dart b/lib/models/redaction.dart index ab62a08d..2fb3b2fd 100644 --- a/lib/models/redaction.dart +++ b/lib/models/redaction.dart @@ -39,5 +39,5 @@ String prettyDateString(BuildContext context, DateTime date) { // } return AppLocalizations.of(context)!.now; } - return DateFormat.yMd(Platform.localeName).add_jm().format(date); + return DateFormat.yMd(Platform.localeName).add_jm().format(date.toLocal()); } diff --git a/lib/themes/opaque.dart b/lib/themes/opaque.dart index 74fdca15..e012c730 100644 --- a/lib/themes/opaque.dart +++ b/lib/themes/opaque.dart @@ -24,7 +24,7 @@ LoadAssetThemes() async { themes = await loadYamlThemes(); } -OpaqueThemeType getTheme(String themeId, String mode) { +OpaqueThemeType getTheme(String themeId, String mode) { if (themeId == "") { themeId = cwtch_theme; } diff --git a/lib/themes/yamltheme.dart b/lib/themes/yamltheme.dart index c788d50b..98e2304b 100644 --- a/lib/themes/yamltheme.dart +++ b/lib/themes/yamltheme.dart @@ -8,8 +8,6 @@ import 'package:flutter/services.dart'; import 'package:yaml/yaml.dart'; import 'package:path/path.dart' as path; - - Future>> loadYamlThemes() async { final manifestJson = await rootBundle.loadString('AssetManifest.json'); final themesList = json.decode(manifestJson).keys.where((String key) => key.startsWith('assets/themes')); @@ -17,7 +15,7 @@ Future>> loadYamlThemes() async { Map> themes = Map(); for (String themefile in themesList) { - if (themefile.substring(themefile.length-4) != ".yml") { + if (themefile.substring(themefile.length - 4) != ".yml") { continue; } @@ -77,16 +75,16 @@ class YmlTheme extends OpaqueThemeType { Color? getColor(String name) { var val = yml["themes"][mode]["theme"][name]; - if (! (val is int)) { + if (!(val is int)) { val = yml["themes"][mode]["theme"][val] ?? val; } - if (! (val is int)) { + if (!(val is int)) { val = yml["themes"][mode]?["colors"][val] ?? val; } - if (! (val is int)) { + if (!(val is int)) { val = yml["colors"]?[val]; } - if (! (val is int)) { + if (!(val is int)) { return null; } return Color(0xFF000000 + val as int); @@ -95,7 +93,7 @@ class YmlTheme extends OpaqueThemeType { String? getImage(String name) { var val = yml["themes"][mode]["theme"]?[name]; if (val != null) { - return path.join("assets", "themes", yml["themes"]["name"], val); + return path.join("assets", "themes", yml["themes"]["name"], val); } return null; } @@ -137,4 +135,4 @@ class YmlTheme extends OpaqueThemeType { // Images get chatImage => getImage("chatImage") ?? fallbackTheme.chatImage; -} \ No newline at end of file +} diff --git a/lib/views/globalsettingsview.dart b/lib/views/globalsettingsview.dart index 2a6062cc..69829d6f 100644 --- a/lib/views/globalsettingsview.dart +++ b/lib/views/globalsettingsview.dart @@ -83,7 +83,7 @@ class _GlobalSettingsViewState extends State { } Widget _buildSettingsList() { - return Consumer(builder: (context, settings, child) { + return Consumer(builder: (ccontext, settings, child) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints viewportConstraints) { var appIcon = Icon(Icons.info, color: settings.current().mainTextColor); return Scrollbar( diff --git a/lib/views/messageview.dart b/lib/views/messageview.dart index c5044d9c..0f5a18a7 100644 --- a/lib/views/messageview.dart +++ b/lib/views/messageview.dart @@ -125,6 +125,7 @@ class _MessageViewState extends State { } // reset the disconnect button to allow for immediate connection... Provider.of(context, listen: false).lastRetryTime = DateTime.now().subtract(Duration(minutes: 2)); + Provider.of(context, listen: false).contactEvents.add(ContactEvent("Disconnect from Peer")); })); } diff --git a/lib/views/peersettingsview.dart b/lib/views/peersettingsview.dart index 8e1fe04c..ff307427 100644 --- a/lib/views/peersettingsview.dart +++ b/lib/views/peersettingsview.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:ui'; +import 'package:cwtch/config.dart'; import 'package:cwtch/cwtch_icons_icons.dart'; import 'package:cwtch/models/appstate.dart'; import 'package:cwtch/models/contact.dart'; @@ -84,6 +85,13 @@ class _PeerSettingsViewState extends State { } } + List evWidgets = Provider.of(context, listen: false).contactEvents.map((ev) { + return ListTile( + title: Text(ev.summary, style: Provider.of(context).scaleFonts(defaultTextStyle)), + subtitle: Text(ev.timestamp.toLocal().toIso8601String(), style: Provider.of(context).scaleFonts(defaultTextStyle)), + ); + }).toList(); + return Scrollbar( trackVisibility: true, controller: peerSettingsScrollController, @@ -319,8 +327,14 @@ class _PeerSettingsViewState extends State { style: settings.scaleFonts(defaultTextButtonStyle.copyWith(decoration: TextDecoration.underline)), ), )) - ]) - ]) + ]), + ]), + Visibility( + visible: EnvironmentConfig.BUILD_VER == dev_version, + maintainSize: false, + child: Column( + children: evWidgets, + )) ]))))); }); }); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 75623952..e71d01e0 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -75,6 +75,7 @@ class _MessageListState extends State { .AttemptReconnection(Provider.of(context, listen: false).onion, Provider.of(context, listen: false).onion); } Provider.of(context, listen: false).lastRetryTime = DateTime.now(); + Provider.of(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection")); setState(() { // force update of this view...otherwise the button won't be removed fast enough... }); @@ -116,8 +117,8 @@ class _MessageListState extends State { // Only show broken heart is the contact is offline... decoration: BoxDecoration( image: Provider.of(outerContext).isOnline() - ? (Provider.of(context).theme.chatImage != null) ? - DecorationImage( + ? (Provider.of(context).theme.chatImage != null) + ? DecorationImage( repeat: ImageRepeat.repeat, image: AssetImage(Provider.of(context).theme.chatImage), colorFilter: ColorFilter.mode(Provider.of(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn)) diff --git a/run-tests.sh b/run-tests.sh index eb9c68c5..a3b3b696 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -5,11 +5,11 @@ sed "s|featurePaths: REPLACED_BY_SCRIPT|featurePaths: [$paths]|" integra flutter pub run build_runner clean flutter pub run build_runner build --delete-conflicting-outputs -PATH=$PATH:$PWD/linux/Tor -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"$PWD/linux/":"$PWD/linux/Tor/" -PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH LOG_FILE=test.log CWTCH_HOME=$PWD/integration_test/env/temp/ flutter test -d linux --dart-define TEST_MODE=true integration_test/gherkin_suite_test.dart -#node index2.js -#if [ "$HEADLESS" = "false" ]; then +PATH=$PATH:"$PWD/linux/Tor" +LD_LIBRARY_PATH="$PWD/linux/":"$PWD/linux/Tor/":$LD_LIBRARY_PATH +env PATH=$PATH LD_LIBRARY_PATH=$LD_LIBRARY_PATH LOG_FILE=test.log CWTCH_HOME=$PWD/integration_test/env/temp/ flutter test -d linux --dart-define TEST_MODE=true integration_test/gherkin_suite_test.dart +# node index2.js +# if [ "$HEADLESS" = "false" ]; then # xdg-open integration_test/gherkin/reports/cucumber_report.html -#fi +# fi