Support Conversation Search, Upgrade Cwtch, Patch support for downloading new Cwtch library name formats
continuous-integration/drone/pr Build is pending Details

This commit is contained in:
Sarah Jamie Lewis 2023-08-02 09:43:27 -07:00
parent 3d9d707b83
commit 6188dffbc0
15 changed files with 157 additions and 15 deletions

View File

@ -1 +1 @@
2023-07-13-19-54-v0.0.5-4-g2e7a9be
2023-08-02-09-30-v0.0.5-13-g6fdcf5b

View File

@ -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
# 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

View File

@ -143,4 +143,7 @@ abstract class Cwtch {
Future<String> TranslateMessage(String profile, int conversation, int message, String language);
bool IsBlodeuweddSupported();
// ignore: non_constant_identifier_names
Future<String> SearchConversations(String profile, String pattern);
}

View File

@ -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");
}

View File

@ -788,7 +788,7 @@ class CwtchFfi implements Cwtch {
@override
// ignore: non_constant_identifier_names
Future<String> GetMessageByID(String profile, int handle, int index) async {
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessageByID");
var getMessageC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_GetMessageById");
// ignore: non_constant_identifier_names
final GetMessage = getMessageC.asFunction<GetJsonBlobFromStrIntIntFn>();
final utf8profile = profile.toNativeUtf8();
@ -1027,4 +1027,20 @@ class CwtchFfi implements Cwtch {
malloc.free(utf8profile);
malloc.free(utf8onion);
}
@override
Future<String> SearchConversations(String profile, String pattern) async {
var searchConversationsC = library.lookup<NativeFunction<string_string_to_string_function>>("c_SearchConversations");
// ignore: non_constant_identifier_names
final SearchConversations = searchConversationsC.asFunction<StringFromStringStringFn>();
final utf8profile = profile.toNativeUtf8();
final utf8pattern = pattern.toNativeUtf8();
EnvironmentConfig.debugLog("Searching for $profile $pattern");
Pointer<Utf8> searchIDRaw = SearchConversations(utf8profile, utf8profile.length, utf8pattern, utf8pattern.length);
String searchID = searchIDRaw.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(searchIDRaw);
malloc.free(utf8profile);
malloc.free(utf8pattern);
return searchID;
}
}

View File

@ -415,4 +415,9 @@ class CwtchGomobile implements Cwtch {
void AttemptReconnection(String profile, String onion) {
cwtchPlatform.invokeMethod("PeerWithOnion", {"ProfileOnion": profile, "onion": onion});
}
@override
Future<String> SearchConversations(String profile, String pattern) async {
return await cwtchPlatform.invokeMethod("SearchConversations", {"ProfileOnion": profile, "pattern": pattern});
}
}

View File

@ -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;

View File

@ -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:

View File

@ -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);
}

View File

@ -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<SearchResult> 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") {

6
lib/models/search.dart Normal file
View File

@ -0,0 +1,6 @@
class SearchResult {
String searchID;
int conversationIdentifier;
int messageIndex;
SearchResult({required this.searchID, required this.conversationIdentifier, required this.messageIndex});
}

View File

@ -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<AppState>(context, listen: false).selectedSearchMessage = messageIndex;
Provider.of<AppState>(context, listen: false).initialScrollIndex = messageIndex!;
EnvironmentConfig.debugLog("Looked up index $messageIndex");
}
if (handle == Provider.of<AppState>(context, listen: false).selectedConversation) {
if (messageIndex != null) {
Provider.of<ContactInfoState>(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<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.unreadMessages;
var previouslySelected = Provider.of<AppState>(context, listen: false).selectedConversation;
@ -48,9 +64,12 @@ void selectConversation(BuildContext context, int handle) {
Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(handle)!.selected();
// triggers update in Double/TripleColumnView
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
Provider.of<ContactInfoState>(context, listen: false).hoveredIndex = -1;
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
if (index != null) {
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
}
// if in singlepane mode, push to the stack
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
if (Provider.of<Settings>(context, listen: false).uiColumns(isLandscape).length == 1) _pushMessageView(context, handle);
@ -287,6 +306,10 @@ class _ContactsViewState extends State<ContactsView> {
controller: ctrlrFilter,
hintText: AppLocalizations.of(context)!.search,
onChanged: (newVal) {
String profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
Provider.of<FlwtchState>(context, listen: false).cwtch.SearchConversations(profileHandle, newVal).then((value) {
Provider.of<ProfileInfoState>(context, listen: false).newSearch(value);
});
Provider.of<ContactListState>(context, listen: false).filter = newVal;
},
);
@ -294,7 +317,7 @@ class _ContactsViewState extends State<ContactsView> {
}
Widget _buildContactList() {
final tiles = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
var tilesSearchResult = Provider.of<ContactListState>(context).filteredList().map((ContactInfoState contact) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: contact),
@ -310,15 +333,28 @@ class _ContactsViewState extends State<ContactsView> {
initialScroll = 0;
}
if (showSearchBar) {
List<SearchResult> searchResults = Provider.of<ProfileInfoState>(context).activeSearchResults;
tilesSearchResult = searchResults.map((SearchResult searchResult) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).contactList.getContact(searchResult.conversationIdentifier)),
ChangeNotifierProvider.value(value: Provider.of<ProfileInfoState>(context).serverList),
],
builder: (context, child) => ContactRow(messageIndex: searchResult.messageIndex),
);
});
} else {}
var contactList = ScrollablePositionedList.separated(
itemScrollController: Provider.of<ProfileInfoState>(context).contactListScrollController,
itemCount: Provider.of<ContactListState>(context).numFiltered,
itemCount: tilesSearchResult.length,
initialScrollIndex: initialScroll,
shrinkWrap: true,
physics: BouncingScrollPhysics(),
semanticChildCount: Provider.of<ContactListState>(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);

View File

@ -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<ContactRow> {
bool isHover = false;
Message? cachedMessage = null;
@override
Widget build(BuildContext context) {
var contact = Provider.of<ContactInfoState>(context);
if (widget.messageIndex != null && this.cachedMessage == null) {
messageHandler(context, Provider.of<ProfileInfoState>(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<ContactRow> {
));
}
bool selected = Provider.of<AppState>(context).selectedConversation == contact.identifier;
if (selected && widget.messageIndex != null) {
if (selected && widget.messageIndex == Provider.of<AppState>(context).selectedSearchMessage) {
selected = true;
} else {
selected = false;
}
}
return InkWell(
enableFeedback: true,
splashFactory: InkSplash.splashFactory,
child: Ink(
color: Provider.of<AppState>(context).selectedConversation == contact.identifier ? Provider.of<Settings>(context).theme.backgroundHilightElementColor : Colors.transparent,
color: selected ? Provider.of<Settings>(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<ContactRow> {
overflow: TextOverflow.ellipsis,
style: TextStyle(color: contact.isBlocked ? Provider.of<Settings>(context).theme.portraitBlockedTextColor : Provider.of<Settings>(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<ContactRow> {
icon: Icon(Icons.block, color: Provider.of<Settings>(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<ContactRow> {
])),
onTap: () {
setState(() {
selectConversation(context, contact.identifier);
selectConversation(context, contact.identifier, widget.messageIndex);
});
},
onHover: (hover) {

View File

@ -31,6 +31,8 @@ class _MessageListState extends State<MessageList> {
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
}
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
//MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";

View File

@ -342,7 +342,7 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
if (id == null) {
// Can't happen
} else {
selectConversation(context, id);
selectConversation(context, id, null);
var contactIndex = Provider.of<ProfileInfoState>(context, listen: false).contactList.filteredList().indexWhere((element) => element.identifier == id);
Provider.of<ProfileInfoState>(context, listen: false).contactListScrollController.jumpTo(index: contactIndex);
}