From 6188dffbc098953cdfe6ba3001ec1ede7ade0016 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 2 Aug 2023 09:43:27 -0700 Subject: [PATCH 1/2] Support Conversation Search, Upgrade Cwtch, Patch support for downloading new Cwtch library name formats --- LIBCWTCH-GO.version | 2 +- fetch-libcwtch-go.sh | 3 ++- lib/cwtch/cwtch.dart | 3 +++ lib/cwtch/cwtchNotifier.dart | 9 ++++++- lib/cwtch/ffi.dart | 18 ++++++++++++- lib/cwtch/gomobile.dart | 5 ++++ lib/models/appstate.dart | 7 +++++ lib/models/contact.dart | 7 +++++ lib/models/message.dart | 1 + lib/models/profile.dart | 19 ++++++++++++++ lib/models/search.dart | 6 +++++ lib/views/contactsview.dart | 50 +++++++++++++++++++++++++++++++----- lib/widgets/contactrow.dart | 38 ++++++++++++++++++++++++--- lib/widgets/messagelist.dart | 2 ++ lib/widgets/messagerow.dart | 2 +- 15 files changed, 157 insertions(+), 15 deletions(-) create mode 100644 lib/models/search.dart diff --git a/LIBCWTCH-GO.version b/LIBCWTCH-GO.version index a24c7e3e..14d429ea 100644 --- a/LIBCWTCH-GO.version +++ b/LIBCWTCH-GO.version @@ -1 +1 @@ -2023-07-13-19-54-v0.0.5-4-g2e7a9be \ No newline at end of file +2023-08-02-09-30-v0.0.5-13-g6fdcf5b \ No newline at end of file diff --git a/fetch-libcwtch-go.sh b/fetch-libcwtch-go.sh index cd660451..d934214e 100755 --- a/fetch-libcwtch-go.sh +++ b/fetch-libcwtch-go.sh @@ -4,4 +4,5 @@ VERSION=`cat LIBCWTCH-GO.version` echo $VERSION curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/android/cwtch.aar --output android/cwtch/cwtch.aar -curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/linux/libCwtch.so --output linux/libCwtch.so \ No newline at end of file +# FIXME...at some point we need to support different linux architectures...for now rely on existing expectations and rename x64 lib +curl --fail https://build.openprivacy.ca/files/libCwtch-autobindings-$VERSION/linux/libCwtch.x64.so --output linux/libCwtch.so \ No newline at end of file diff --git a/lib/cwtch/cwtch.dart b/lib/cwtch/cwtch.dart index 98e4fac5..d9e236fc 100644 --- a/lib/cwtch/cwtch.dart +++ b/lib/cwtch/cwtch.dart @@ -143,4 +143,7 @@ abstract class Cwtch { Future TranslateMessage(String profile, int conversation, int message, String language); bool IsBlodeuweddSupported(); + + // ignore: non_constant_identifier_names + Future SearchConversations(String profile, String pattern); } diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index f32aced3..d545ad36 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -59,7 +59,7 @@ class CwtchNotifier { } void handleMessage(String type, dynamic data) { - // EnvironmentConfig.debugLog("NewEvent $type $data"); + //EnvironmentConfig.debugLog("NewEvent $type $data"); switch (type) { case "CwtchStarted": appState.SetCwtchInit(); @@ -324,6 +324,7 @@ class CwtchNotifier { torStatus.updateVersion(data["Data"]); break; case "UpdateServerInfo": + EnvironmentConfig.debugLog("NewEvent UpdateServerInfo $type $data"); profileCN.getProfile(data["ProfileOnion"])?.replaceServers(data["ServerList"]); break; case "TokenManagerInfo": @@ -460,6 +461,12 @@ class CwtchNotifier { profileCN.getProfile(data["ProfileOnion"])?.contactList.findContact(handle)?.acnCircuit = data["Data"]; } break; + case "SearchResult": + String searchID = data["SearchID"]; + var conversationIdentifier = int.parse(data["ConversationID"]); + var messageIndex = int.parse(data["RowIndex"]); + profileCN.getProfile(data["ProfileOnion"])?.handleSearchResult(searchID, conversationIdentifier, messageIndex); + break; default: EnvironmentConfig.debugLog("unhandled event: $type"); } diff --git a/lib/cwtch/ffi.dart b/lib/cwtch/ffi.dart index e1e37a8a..b14b790c 100644 --- a/lib/cwtch/ffi.dart +++ b/lib/cwtch/ffi.dart @@ -788,7 +788,7 @@ class CwtchFfi implements Cwtch { @override // ignore: non_constant_identifier_names Future GetMessageByID(String profile, int handle, int index) async { - var getMessageC = library.lookup>("c_GetMessageByID"); + var getMessageC = library.lookup>("c_GetMessageById"); // ignore: non_constant_identifier_names final GetMessage = getMessageC.asFunction(); final utf8profile = profile.toNativeUtf8(); @@ -1027,4 +1027,20 @@ class CwtchFfi implements Cwtch { malloc.free(utf8profile); malloc.free(utf8onion); } + + @override + Future SearchConversations(String profile, String pattern) async { + var searchConversationsC = library.lookup>("c_SearchConversations"); + // ignore: non_constant_identifier_names + final SearchConversations = searchConversationsC.asFunction(); + final utf8profile = profile.toNativeUtf8(); + final utf8pattern = pattern.toNativeUtf8(); + EnvironmentConfig.debugLog("Searching for $profile $pattern"); + Pointer searchIDRaw = SearchConversations(utf8profile, utf8profile.length, utf8pattern, utf8pattern.length); + String searchID = searchIDRaw.toDartString(); + _UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(searchIDRaw); + malloc.free(utf8profile); + malloc.free(utf8pattern); + return searchID; + } } diff --git a/lib/cwtch/gomobile.dart b/lib/cwtch/gomobile.dart index 7278ffeb..010a2bfe 100644 --- a/lib/cwtch/gomobile.dart +++ b/lib/cwtch/gomobile.dart @@ -415,4 +415,9 @@ class CwtchGomobile implements Cwtch { void AttemptReconnection(String profile, String onion) { cwtchPlatform.invokeMethod("PeerWithOnion", {"ProfileOnion": profile, "onion": onion}); } + + @override + Future SearchConversations(String profile, String pattern) async { + return await cwtchPlatform.invokeMethod("SearchConversations", {"ProfileOnion": profile, "pattern": pattern}); + } } diff --git a/lib/models/appstate.dart b/lib/models/appstate.dart index 97c1fd9c..22e5cbf3 100644 --- a/lib/models/appstate.dart +++ b/lib/models/appstate.dart @@ -12,6 +12,7 @@ class AppState extends ChangeNotifier { String appError = ""; String? _selectedProfile; int? _selectedConversation; + int? _selectedSearchMessage; int _initialScrollIndex = 0; bool _unreadMessagesBelow = false; bool _disableFilePicker = false; @@ -54,6 +55,12 @@ class AppState extends ChangeNotifier { notifyListeners(); } + int? get selectedSearchMessage => _selectedSearchMessage; + set selectedSearchMessage(int? newVal) { + this._selectedSearchMessage = newVal; + notifyListeners(); + } + bool get disableFilePicker => _disableFilePicker; set disableFilePicker(bool newVal) { this._disableFilePicker = newVal; diff --git a/lib/models/contact.dart b/lib/models/contact.dart index ab1e65a5..00499dc1 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -68,6 +68,7 @@ class ContactInfoState extends ChangeNotifier { MessageDraft _messageDraft = MessageDraft.empty(); var _hoveredIndex = -1; + var _pendingScroll = -1; ContactInfoState( this.profileOnion, @@ -438,6 +439,12 @@ class ContactInfoState extends ChangeNotifier { notifyListeners(); } + int get pendingScroll => _pendingScroll; + set pendingScroll(int newVal) { + this._pendingScroll = newVal; + notifyListeners(); + } + String statusString(BuildContext context) { switch (this.availabilityStatus) { case ProfileStatusMenu.available: diff --git a/lib/models/message.dart b/lib/models/message.dart index 316e0fbf..decdf51b 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -160,6 +160,7 @@ class ById implements CacheHandler { if (messageInfo == null) { return Future.value(null); } + EnvironmentConfig.debugLog("fetching $profileOnion $conversationIdentifier $id ${messageInfo.wrapper}"); cache.addUnindexed(messageInfo); return Future.value(messageInfo); } diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 86ef3f2b..fed1b3a6 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cwtch/config.dart'; import 'package:cwtch/models/remoteserver.dart'; +import 'package:cwtch/models/search.dart'; import 'package:flutter/widgets.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -10,6 +11,7 @@ import '../views/contactsview.dart'; import 'contact.dart'; import 'contactlist.dart'; import 'filedownloadprogress.dart'; +import 'message.dart'; import 'messagecache.dart'; import 'profileservers.dart'; @@ -91,6 +93,23 @@ class ProfileInfoState extends ChangeNotifier { } } + // Code for managing the state of the profile-wide search feature... + String activeSearchID = ""; + List activeSearchResults = List.empty(growable: true); + + void newSearch(String activeSearchID) { + this.activeSearchID = activeSearchID; + this.activeSearchResults.clear(); + notifyListeners(); + } + + void handleSearchResult(String searchID, int conversationIdentifier, int messageIndex) { + if (searchID == activeSearchID) { + activeSearchResults.add(SearchResult(searchID: searchID, conversationIdentifier: conversationIdentifier, messageIndex: messageIndex)); + notifyListeners(); + } + } + // Parse out the server list json into our server info state struct... void replaceServers(String serversJson) { if (serversJson != "" && serversJson != "null") { diff --git a/lib/models/search.dart b/lib/models/search.dart new file mode 100644 index 00000000..62a3dc85 --- /dev/null +++ b/lib/models/search.dart @@ -0,0 +1,6 @@ +class SearchResult { + String searchID; + int conversationIdentifier; + int messageIndex; + SearchResult({required this.searchID, required this.conversationIdentifier, required this.messageIndex}); +} diff --git a/lib/views/contactsview.dart b/lib/views/contactsview.dart index 8845b837..4687b0db 100644 --- a/lib/views/contactsview.dart +++ b/lib/views/contactsview.dart @@ -7,6 +7,7 @@ import 'package:cwtch/models/contact.dart'; import 'package:cwtch/models/contactlist.dart'; import 'package:cwtch/models/profile.dart'; import 'package:cwtch/models/profilelist.dart'; +import 'package:cwtch/models/search.dart'; import 'package:cwtch/views/profileserversview.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/widgets/contactrow.dart'; @@ -15,7 +16,9 @@ import 'package:cwtch/widgets/textfield.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../config.dart'; import '../main.dart'; +import '../models/message.dart'; import '../settings.dart'; import 'addcontactview.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -35,10 +38,23 @@ class ContactsView extends StatefulWidget { } // selectConversation can be called from anywhere to set the active conversation -void selectConversation(BuildContext context, int handle) { +void selectConversation(BuildContext context, int handle, int? messageIndex) { + int? index = null; + + if (messageIndex != null) { + // this message is loaded + Provider.of(context, listen: false).selectedSearchMessage = messageIndex; + Provider.of(context, listen: false).initialScrollIndex = messageIndex!; + EnvironmentConfig.debugLog("Looked up index $messageIndex"); + } + if (handle == Provider.of(context, listen: false).selectedConversation) { + if (messageIndex != null) { + Provider.of(context, listen: false).messageScrollController.scrollTo(index: messageIndex, duration: Duration(milliseconds: 100)); + } return; } + // requery instead of using contactinfostate directly because sometimes listview gets confused about data that resorts var unread = Provider.of(context, listen: false).contactList.getContact(handle)!.unreadMessages; var previouslySelected = Provider.of(context, listen: false).selectedConversation; @@ -48,9 +64,12 @@ void selectConversation(BuildContext context, int handle) { Provider.of(context, listen: false).contactList.getContact(handle)!.selected(); // triggers update in Double/TripleColumnView - Provider.of(context, listen: false).initialScrollIndex = unread; - Provider.of(context, listen: false).selectedConversation = handle; Provider.of(context, listen: false).hoveredIndex = -1; + Provider.of(context, listen: false).selectedConversation = handle; + if (index != null) { + Provider.of(context, listen: false).initialScrollIndex = unread; + } + // if in singlepane mode, push to the stack var isLandscape = Provider.of(context, listen: false).isLandscape(context); if (Provider.of(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle); @@ -287,6 +306,10 @@ class _ContactsViewState extends State { controller: ctrlrFilter, hintText: AppLocalizations.of(context)!.search, onChanged: (newVal) { + String profileHandle = Provider.of(context, listen: false).onion; + Provider.of(context, listen: false).cwtch.SearchConversations(profileHandle, newVal).then((value) { + Provider.of(context, listen: false).newSearch(value); + }); Provider.of(context, listen: false).filter = newVal; }, ); @@ -294,7 +317,7 @@ class _ContactsViewState extends State { } Widget _buildContactList() { - final tiles = Provider.of(context).filteredList().map((ContactInfoState contact) { + var tilesSearchResult = Provider.of(context).filteredList().map((ContactInfoState contact) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: contact), @@ -310,15 +333,28 @@ class _ContactsViewState extends State { initialScroll = 0; } + if (showSearchBar) { + List searchResults = Provider.of(context).activeSearchResults; + tilesSearchResult = searchResults.map((SearchResult searchResult) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: Provider.of(context).contactList.getContact(searchResult.conversationIdentifier)), + ChangeNotifierProvider.value(value: Provider.of(context).serverList), + ], + builder: (context, child) => ContactRow(messageIndex: searchResult.messageIndex), + ); + }); + } else {} + var contactList = ScrollablePositionedList.separated( itemScrollController: Provider.of(context).contactListScrollController, - itemCount: Provider.of(context).numFiltered, + itemCount: tilesSearchResult.length, initialScrollIndex: initialScroll, shrinkWrap: true, physics: BouncingScrollPhysics(), - semanticChildCount: Provider.of(context).numFiltered, + semanticChildCount: tilesSearchResult.length, itemBuilder: (context, index) { - return tiles.elementAt(index); + return tilesSearchResult.elementAt(index); }, separatorBuilder: (BuildContext context, int index) { return Divider(height: 1); diff --git a/lib/widgets/contactrow.dart b/lib/widgets/contactrow.dart index bdcc203d..3cae0045 100644 --- a/lib/widgets/contactrow.dart +++ b/lib/widgets/contactrow.dart @@ -10,21 +10,34 @@ import 'package:cwtch/widgets/profileimage.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../main.dart'; +import '../models/message.dart'; import '../settings.dart'; import 'package:intl/intl.dart'; class ContactRow extends StatefulWidget { + int? messageIndex; + + ContactRow({this.messageIndex}); @override _ContactRowState createState() => _ContactRowState(); } class _ContactRowState extends State { bool isHover = false; + Message? cachedMessage = null; @override Widget build(BuildContext context) { var contact = Provider.of(context); + if (widget.messageIndex != null && this.cachedMessage == null) { + messageHandler(context, Provider.of(context, listen: false).onion, contact.identifier, ByIndex(widget.messageIndex!)).then((value) { + setState(() { + this.cachedMessage = value; + }); + }); + } + // Only groups have a sync status Widget? syncStatus; if (contact.isGroup) { @@ -37,11 +50,19 @@ class _ContactRowState extends State { )); } + bool selected = Provider.of(context).selectedConversation == contact.identifier; + if (selected && widget.messageIndex != null) { + if (selected && widget.messageIndex == Provider.of(context).selectedSearchMessage) { + selected = true; + } else { + selected = false; + } + } return InkWell( enableFeedback: true, splashFactory: InkSplash.splashFactory, child: Ink( - color: Provider.of(context).selectedConversation == contact.identifier ? Provider.of(context).theme.backgroundHilightElementColor : Colors.transparent, + color: selected ? Provider.of(context).theme.backgroundHilightElementColor : Colors.transparent, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.all(6.0), //border size @@ -85,6 +106,17 @@ class _ContactRowState extends State { overflow: TextOverflow.ellipsis, style: TextStyle(color: contact.isBlocked ? Provider.of(context).theme.portraitBlockedTextColor : Provider.of(context).theme.mainTextColor)), ), + // we need to ignore the child widget in this context, otherwise gesture events will flow down... + IgnorePointer( + ignoring: true, + child: Visibility( + visible: this.cachedMessage != null, + maintainSize: false, + maintainInteractivity: false, + maintainSemantics: false, + maintainState: false, + child: this.cachedMessage == null ? CircularProgressIndicator() : this.cachedMessage!.getPreviewWidget(context), + )), Container( padding: EdgeInsets.all(0), child: contact.isInvitation == true @@ -124,7 +156,7 @@ class _ContactRowState extends State { icon: Icon(Icons.block, color: Provider.of(context).theme.mainTextColor), onPressed: () {}, ) - : Text(dateToNiceString(contact.lastMessageTime))), + : Text(dateToNiceString(widget.messageIndex == null ? contact.lastMessageTime : (this.cachedMessage?.getMetadata().timestamp ?? DateTime.now())))), ), ], ))), @@ -149,7 +181,7 @@ class _ContactRowState extends State { ])), onTap: () { setState(() { - selectConversation(context, contact.identifier); + selectConversation(context, contact.identifier, widget.messageIndex); }); }, onHover: (hover) { diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 257afc22..9fe8bed2 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -31,6 +31,8 @@ class _MessageListState extends State { ByIndex(0).loadUnsynced(Provider.of(context, listen: false).cwtch, Provider.of(outerContext, listen: false).selectedProfile!, conversationId, cache!); } var initi = Provider.of(outerContext, listen: false).initialScrollIndex; + //MessageCache? cache = Provider.of(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache; + bool isP2P = !Provider.of(context).isGroup; bool isGroupAndSyncing = Provider.of(context).isGroup == true && Provider.of(context).status == "Authenticated"; diff --git a/lib/widgets/messagerow.dart b/lib/widgets/messagerow.dart index af9d36ca..d85dab25 100644 --- a/lib/widgets/messagerow.dart +++ b/lib/widgets/messagerow.dart @@ -342,7 +342,7 @@ class MessageRowState extends State with SingleTickerProviderStateMi if (id == null) { // Can't happen } else { - selectConversation(context, id); + selectConversation(context, id, null); var contactIndex = Provider.of(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == id); Provider.of(context, listen: false).contactListScrollController.jumpTo(index: contactIndex); } From dee5752d3887b1a913c2cee9516f4a7c8d2e1c29 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Wed, 2 Aug 2023 09:49:36 -0700 Subject: [PATCH 2/2] Cleanup + Android Support --- android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt | 6 ++++++ lib/cwtch/cwtchNotifier.dart | 2 +- lib/widgets/messagelist.dart | 2 -- 3 files changed, 7 insertions(+), 3 deletions(-) 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 c4cdda25..f0a49033 100644 --- a/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt +++ b/android/app/src/main/kotlin/im/cwtch/flwtch/MainActivity.kt @@ -517,6 +517,12 @@ class MainActivity: FlutterActivity() { result.success(Cwtch.importProfile(file, pass)) return } + "SearchConversations" -> { + val profile: String = call.argument("ProfileOnion") ?: "" + val pattern: String = call.argument("pattern") ?: "" + result.success(Cwtch.searchConversations(profile, pattern)) + return + } "ReconnectCwtchForeground" -> { Cwtch.reconnectCwtchForeground() } diff --git a/lib/cwtch/cwtchNotifier.dart b/lib/cwtch/cwtchNotifier.dart index d545ad36..6c48edb5 100644 --- a/lib/cwtch/cwtchNotifier.dart +++ b/lib/cwtch/cwtchNotifier.dart @@ -59,7 +59,7 @@ class CwtchNotifier { } void handleMessage(String type, dynamic data) { - //EnvironmentConfig.debugLog("NewEvent $type $data"); + // EnvironmentConfig.debugLog("NewEvent $type $data"); switch (type) { case "CwtchStarted": appState.SetCwtchInit(); diff --git a/lib/widgets/messagelist.dart b/lib/widgets/messagelist.dart index 9fe8bed2..257afc22 100644 --- a/lib/widgets/messagelist.dart +++ b/lib/widgets/messagelist.dart @@ -31,8 +31,6 @@ class _MessageListState extends State { ByIndex(0).loadUnsynced(Provider.of(context, listen: false).cwtch, Provider.of(outerContext, listen: false).selectedProfile!, conversationId, cache!); } var initi = Provider.of(outerContext, listen: false).initialScrollIndex; - //MessageCache? cache = Provider.of(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache; - bool isP2P = !Provider.of(context).isGroup; bool isGroupAndSyncing = Provider.of(context).isGroup == true && Provider.of(context).status == "Authenticated";