Merge pull request 'message cache allows index locking, rework messageHandler to use bulk fetching, sendMessage flow with no sleep; move some core getMessages/SendMessage handlers from FlwtchWorker to MainActivity' (#407) from androMessage into trunk
continuous-integration/drone/push Build is passing Details

Reviewed-on: #407
Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
This commit is contained in:
Dan Ballard 2022-03-24 19:41:16 +00:00
commit b8c1c7682b
15 changed files with 448 additions and 284 deletions

View File

@ -1 +1 @@
2022-03-22-17-15-v1.6.0-11-gca4897b 2022-03-23-19-06-v1.6.0-13-g1acae32

View File

@ -1 +1 @@
2022-03-22-21-16-v1.6.0-11-gca4897b 2022-03-23-23-06-v1.6.0-13-g1acae32

View File

@ -290,25 +290,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
val conversation = a.getInt("conversation").toLong() val conversation = a.getInt("conversation").toLong()
Cwtch.unblockContact(profile, conversation) Cwtch.unblockContact(profile, conversation)
} }
"SendMessage" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val message = (a.get("message") as? String) ?: ""
Log.i(TAG, "SendMessage: $message")
Cwtch.sendMessage(profile, conversation, message)
}
"SendInvitation" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val target = a.getInt("target").toLong()
Cwtch.sendInvitation(profile, conversation, target)
}
"ShareFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong()
val filepath = (a.get("filepath") as? String) ?: ""
Cwtch.shareFile(profile, conversation, filepath)
}
"DownloadFile" -> { "DownloadFile" -> {
val profile = (a.get("ProfileOnion") as? String) ?: "" val profile = (a.get("ProfileOnion") as? String) ?: ""
val conversation = a.getInt("conversation").toLong() val conversation = a.getInt("conversation").toLong()

View File

@ -35,6 +35,9 @@ import android.os.Environment
import android.database.Cursor import android.database.Cursor
import android.provider.MediaStore import android.provider.MediaStore
import cwtch.Cwtch
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
override fun provideSplashScreen(): SplashScreen? = SplashView() override fun provideSplashScreen(): SplashScreen? = SplashView()
@ -168,84 +171,117 @@ class MainActivity: FlutterActivity() {
// receives messages from the ForegroundService (which provides, ironically enough, the backend) // receives messages from the ForegroundService (which provides, ironically enough, the backend)
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) { private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
var method = call.method var method = call.method
// todo change usage patern to match that in FlwtchWorker
// Unsafe for anything using int args, causes access time attempt to cast to string which will fail
val argmap: Map<String, String> = call.arguments as Map<String, String> val argmap: Map<String, String> = call.arguments as Map<String, String>
// the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start() // the frontend calls Start every time it fires up, but we don't want to *actually* call Cwtch.Start()
// in case the ForegroundService is still running. in both cases, however, we *do* want to re-register // in case the ForegroundService is still running. in both cases, however, we *do* want to re-register
// the eventbus listener. // the eventbus listener.
if (call.method == "Start") { when (call.method) {
val uniqueTag = argmap["torPath"] ?: "nullEventBus" "Start" -> {
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
// note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get // note: because the ForegroundService is specified as UniquePeriodicWork, it can't actually get
// accidentally duplicated. however, we still need to manually check if it's running or not, so // accidentally duplicated. however, we still need to manually check if it's running or not, so
// that we can divert this method call to ReconnectCwtchForeground instead if so. // that we can divert this method call to ReconnectCwtchForeground instead if so.
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get() val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
for (workInfo in works) { for (workInfo in works) {
WorkManager.getInstance(this).cancelWorkById(workInfo.id) WorkManager.getInstance(this).cancelWorkById(workInfo.id)
}
WorkManager.getInstance(this).pruneWork()
Log.i("MainActivity.kt", "Start() launching foregroundservice")
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
return
} else if (call.method == "CreateDownloadableFile") {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
return
} else if (call.method == "ExportPreviewedFile") {
this.exportFromPath = argmap["Path"] ?: ""
val suggestion = argmap["FileName"] ?: "filename.ext"
var imgType = "jpeg"
if (suggestion.endsWith("png")) {
imgType = "png"
} else if (suggestion.endsWith("webp")) {
imgType = "webp"
} else if (suggestion.endsWith("bmp")) {
imgType = "bmp"
} else if (suggestion.endsWith("gif")) {
imgType = "gif"
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/" + imgType
putExtra(Intent.EXTRA_TITLE, suggestion)
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
return
} else if (call.method == "ExportProfile") {
this.exportFromPath = argmap["file"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/gzip"
putExtra(Intent.EXTRA_TITLE, argmap["file"])
}
startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
}
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
} }
WorkManager.getInstance(this).pruneWork()
Log.i("MainActivity.kt", "Start() launching foregroundservice")
// this is where the eventbus ForegroundService gets launched. WorkManager should keep it alive after this
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, call.method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
// 15 minutes is the shortest interval you can request
val workRequest = PeriodicWorkRequestBuilder<FlwtchWorker>(15, TimeUnit.MINUTES).setInputData(data).addTag(WORKER_TAG).addTag(uniqueTag).build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork("req_$uniqueTag", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
} }
) "CreateDownloadableFile" -> {
this.dlToProfile = argmap["ProfileOnion"] ?: ""
this.dlToHandle = argmap["handle"] ?: ""
val suggestedName = argmap["filename"] ?: "filename.ext"
this.dlToFileKey = argmap["filekey"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/octet-stream"
putExtra(Intent.EXTRA_TITLE, suggestedName)
}
startActivityForResult(intent, FILEPICKER_REQUEST_CODE)
}
"ExportPreviewedFile" -> {
this.exportFromPath = argmap["Path"] ?: ""
val suggestion = argmap["FileName"] ?: "filename.ext"
var imgType = "jpeg"
if (suggestion.endsWith("png")) {
imgType = "png"
} else if (suggestion.endsWith("webp")) {
imgType = "webp"
} else if (suggestion.endsWith("bmp")) {
imgType = "bmp"
} else if (suggestion.endsWith("gif")) {
imgType = "gif"
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "image/" + imgType
putExtra(Intent.EXTRA_TITLE, suggestion)
}
startActivityForResult(intent, PREVIEW_EXPORT_REQUEST_CODE)
}
"ExportProfile" -> {
this.exportFromPath = argmap["file"] ?: ""
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/gzip"
putExtra(Intent.EXTRA_TITLE, argmap["file"])
}
startActivityForResult(intent, PROFILE_EXPORT_REQUEST_CODE)
}
"GetMessages" -> {
Log.d("MainActivity.kt", "Cwtch GetMessages")
val profile = argmap["ProfileOnion"] ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val indexI: Int = call.argument("index") ?: 0
val count: Int = call.argument("count") ?: 1
result.success(Cwtch.getMessages(profile, conversation.toLong(), indexI.toLong(), count.toLong()))
}
"SendMessage" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val message: String = call.argument("message") ?: ""
result.success(Cwtch.sendMessage(profile, conversation.toLong(), message))
}
"SendInvitation" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val target: Int = call.argument("target") ?: 0
result.success(Cwtch.sendInvitation(profile, conversation.toLong(), target.toLong()))
}
"ShareFile" -> {
val profile: String = call.argument("ProfileOnion") ?: ""
val conversation: Int = call.argument("conversation") ?: 0
val filepath: String = call.argument("filepath") ?: ""
result.success(Cwtch.shareFile(profile, conversation.toLong(), filepath))
}
else -> {
// ...otherwise fallthru to a normal ffi method call (and return the result using the result callback)
val data: Data = Data.Builder().putString(FlwtchWorker.KEY_METHOD, method).putString(FlwtchWorker.KEY_ARGS, JSONObject(argmap).toString()).build()
val workRequest = OneTimeWorkRequestBuilder<FlwtchWorker>().setInputData(data).build()
WorkManager.getInstance(this).enqueue(workRequest)
WorkManager.getInstance(applicationContext).getWorkInfoByIdLiveData(workRequest.id).observe(
this, Observer { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val res = workInfo.outputData.keyValueMap.toString()
result.success(workInfo.outputData.getString("result"))
}
}
)
}
}
} }
// using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver // using onresume/onstop for broadcastreceiver because of extended discussion on https://stackoverflow.com/questions/7439041/how-to-unregister-broadcastreceiver

View File

@ -48,12 +48,15 @@ abstract class Cwtch {
Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash); Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profile, int handle, String message); Future<dynamic> GetMessages(String profile, int handle, int index, int count);
// ignore: non_constant_identifier_names
void SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profile, int handle, String filepath); Future<dynamic> SendMessage(String profile, int handle, String message);
// ignore: non_constant_identifier_names
Future<dynamic> SendInvitation(String profile, int handle, int target);
// ignore: non_constant_identifier_names
Future<dynamic> ShareFile(String profile, int handle, String filepath);
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey); void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
// android-only // android-only

View File

@ -159,7 +159,7 @@ class CwtchNotifier {
var senderHandle = data['RemotePeer']; var senderHandle = data['RemotePeer'];
var senderImage = data['picture']; var senderImage = data['picture'];
var isAuto = data['Auto'] == "true"; var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash']; String contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"]; var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
var selectedConversation = selectedProfile && appState.selectedConversation == identifier; var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
var notification = data["notification"]; var notification = data["notification"];
@ -211,7 +211,7 @@ class CwtchNotifier {
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier); var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
var currentTotal = contact!.totalMessages; var currentTotal = contact!.totalMessages;
var isAuto = data['Auto'] == "true"; var isAuto = data['Auto'] == "true";
String? contenthash = data['ContentHash']; String contenthash = data['ContentHash'];
var selectedProfile = appState.selectedProfile == data["ProfileOnion"]; var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
var selectedConversation = selectedProfile && appState.selectedConversation == identifier; var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
var notification = data["notification"]; var notification = data["notification"];

View File

@ -61,6 +61,9 @@ typedef VoidFromStringIntFn = void Function(Pointer<Utf8>, int, int);
typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length); typedef get_json_blob_string_function = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);
typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len); typedef GetJsonBlobStringFn = Pointer<Utf8> Function(Pointer<Utf8> str, int len);
typedef get_json_blob_from_string_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32 , Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStrFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, Pointer<Utf8>, int);
//func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char { //func GetMessage(profile_ptr *C.char, profile_len C.int, handle_ptr *C.char, handle_len C.int, message_index C.int) *C.char {
typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32); typedef get_json_blob_from_str_str_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Utf8>, Int32, Int32);
typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int); typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Utf8>, int, int);
@ -68,6 +71,9 @@ typedef GetJsonBlobFromStrStrIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int,
typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32); typedef get_json_blob_from_str_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int); typedef GetJsonBlobFromStrIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int);
typedef get_json_blob_from_str_int_int_int_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Int32, Int32);
typedef GetJsonBlobFromStrIntIntIntFn = Pointer<Utf8> Function(Pointer<Utf8>, int, int, int, int);
typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32); typedef get_json_blob_from_str_int_string_function = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Int32, Pointer<Utf8>, Int32);
typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function( typedef GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
Pointer<Utf8>, Pointer<Utf8>,
@ -300,6 +306,19 @@ class CwtchFfi implements Cwtch {
return jsonMessage; return jsonMessage;
} }
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, int handle, int index, int count) async {
var getMessagesC = library.lookup<NativeFunction<get_json_blob_from_str_int_int_int_function>>("c_GetMessages");
// ignore: non_constant_identifier_names
final GetMessages = getMessagesC.asFunction<GetJsonBlobFromStrIntIntIntFn>();
final utf8profile = profile.toNativeUtf8();
Pointer<Utf8> jsonMessageBytes = GetMessages(utf8profile, utf8profile.length, handle, index, count);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(utf8profile);
return jsonMessage;
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String json) { void SendProfileEvent(String onion, String json) {
@ -359,39 +378,48 @@ class CwtchFfi implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, int contactHandle, String message) { Future<dynamic> SendMessage(String profileOnion, int contactHandle, String message) async {
var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage"); var sendMessage = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_SendMessage");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>(); final SendMessage = sendMessage.asFunction<GetJsonBlobFromStrIntStrFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u3 = message.toNativeUtf8(); final u3 = message.toNativeUtf8();
SendMessage(u1, u1.length, contactHandle, u3, u3.length); Pointer<Utf8> jsonMessageBytes = SendMessage(u1, u1.length, contactHandle, u3, u3.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1); malloc.free(u1);
malloc.free(u3); malloc.free(u3);
return jsonMessage;
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, int contactHandle, int target) { Future<dynamic> SendInvitation(String profileOnion, int contactHandle, int target) async {
var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation"); var sendInvitation = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_SendInvitation");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>(); final SendInvitation = sendInvitation.asFunction<GetJsonBlobFromStrIntIntFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
SendInvitation(u1, u1.length, contactHandle, target); Pointer<Utf8> jsonMessageBytes = SendInvitation(u1, u1.length, contactHandle, target);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1); malloc.free(u1);
return jsonMessage;
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int contactHandle, String filepath) { Future<dynamic> ShareFile(String profileOnion, int contactHandle, String filepath) async {
var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile"); var shareFile = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_ShareFile");
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>(); final ShareFile = shareFile.asFunction<GetJsonBlobFromStrIntStrFn>();
final u1 = profileOnion.toNativeUtf8(); final u1 = profileOnion.toNativeUtf8();
final u3 = filepath.toNativeUtf8(); final u3 = filepath.toNativeUtf8();
ShareFile(u1, u1.length, contactHandle, u3, u3.length); Pointer<Utf8> jsonMessageBytes = ShareFile(u1, u1.length, contactHandle, u3, u3.length);
String jsonMessage = jsonMessageBytes.toDartString();
_UnsafeFreePointerAnyUseOfThisFunctionMustBeDoubleApproved(jsonMessageBytes);
malloc.free(u1); malloc.free(u1);
malloc.free(u3); malloc.free(u3);
return jsonMessage;
} }
@override @override

View File

@ -94,6 +94,11 @@ class CwtchGomobile implements Cwtch {
return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id}); return cwtchPlatform.invokeMethod("GetMessageByID", {"ProfileOnion": profile, "conversation": conversation, "id": id});
} }
// ignore: non_constant_identifier_names
Future<dynamic> GetMessages(String profile, int conversation, int index, int count) {
return cwtchPlatform.invokeMethod("GetMessages", {"ProfileOnion": profile, "conversation": conversation, "index": index, "count": count});
}
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendProfileEvent(String onion, String jsonEvent) { void SendProfileEvent(String onion, String jsonEvent) {
@ -129,20 +134,20 @@ class CwtchGomobile implements Cwtch {
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendMessage(String profileOnion, int conversation, String message) { Future<dynamic> SendMessage(String profileOnion, int conversation, String message) {
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message}); return cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void SendInvitation(String profileOnion, int conversation, int target) { Future<dynamic> SendInvitation(String profileOnion, int conversation, int target) {
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target}); return cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
} }
@override @override
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
void ShareFile(String profileOnion, int conversation, String filepath) { Future<dynamic> ShareFile(String profileOnion, int conversation, String filepath) {
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath}); return cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
} }
@override @override

View File

@ -42,7 +42,7 @@ class ContactInfoState extends ChangeNotifier {
late int _totalMessages = 0; late int _totalMessages = 0;
late DateTime _lastMessageTime; late DateTime _lastMessageTime;
late Map<String, GlobalKey<MessageRowState>> keys; late Map<String, GlobalKey<MessageRowState>> keys;
int _newMarker = 0; int _newMarkerMsgId = -1;
DateTime _newMarkerClearAt = DateTime.now(); DateTime _newMarkerClearAt = DateTime.now();
late MessageCache messageCache; late MessageCache messageCache;
@ -82,7 +82,7 @@ class ContactInfoState extends ChangeNotifier {
this._server = server; this._server = server;
this._archived = archived; this._archived = archived;
this._notificationPolicy = notificationPolicyFromString(notificationPolicy); this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
this.messageCache = new MessageCache(); this.messageCache = new MessageCache(_totalMessages);
keys = Map<String, GlobalKey<MessageRowState>>(); keys = Map<String, GlobalKey<MessageRowState>>();
} }
@ -148,41 +148,29 @@ class ContactInfoState extends ChangeNotifier {
int get unreadMessages => this._unreadMessages; int get unreadMessages => this._unreadMessages;
set unreadMessages(int newVal) { set unreadMessages(int newVal) {
// don't reset newMarker position when unreadMessages is being cleared if (newVal == 0) {
if (newVal > 0) { // conversation has been selected, start the countdown for the New Messager marker to be reset
this._newMarker = newVal;
} else {
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2)); this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
} }
this._unreadMessages = newVal; this._unreadMessages = newVal;
notifyListeners(); notifyListeners();
} }
int get newMarker { int get newMarkerMsgId {
if (DateTime.now().isAfter(this._newMarkerClearAt)) { if (DateTime.now().isAfter(this._newMarkerClearAt)) {
// perform heresy // perform heresy
this._newMarker = 0; this._newMarkerMsgId = -1;
// no need to notifyListeners() because presumably this getter is // no need to notifyListeners() because presumably this getter is
// being called from a renderer anyway // being called from a renderer anyway
} }
return this._newMarker; return this._newMarkerMsgId;
}
// what's a getter that sometimes sets without a setter
// that sometimes doesn't set
set newMarker(int newVal) {
// only unreadMessages++ can set newMarker = 1;
// avoids drawing a marker when the convo is already open
if (newVal >= 1) {
this._newMarker = newVal;
notifyListeners();
}
} }
int get totalMessages => this._totalMessages; int get totalMessages => this._totalMessages;
set totalMessages(int newVal) { set totalMessages(int newVal) {
this._totalMessages = newVal; this._totalMessages = newVal;
this.messageCache.storageMessageCount = newVal;
notifyListeners(); notifyListeners();
} }
@ -251,11 +239,12 @@ class ContactInfoState extends ChangeNotifier {
return ret; return ret;
} }
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) { void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedConversation) {
if (!selectedConversation) { if (!selectedConversation) {
unreadMessages++; unreadMessages++;
} else { }
newMarker++; if (_newMarkerMsgId == -1) {
_newMarkerMsgId = messageID;
} }
this._lastMessageTime = timestamp; this._lastMessageTime = timestamp;

View File

@ -123,7 +123,7 @@ class ContactListState extends ChangeNotifier {
return idx >= 0 ? _contacts[idx] : null; return idx >= 0 ? _contacts[idx] : null;
} }
void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedConversation) { void newMessage(int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedConversation) {
getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation); getContact(identifier)?.newMessage(identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, selectedConversation);
updateLastMessageTime(identifier, DateTime.now()); updateLastMessageTime(identifier, DateTime.now());
} }

View File

@ -63,9 +63,7 @@ Message compileOverlay(MessageMetadata metadata, String messageData) {
} }
abstract class CacheHandler { abstract class CacheHandler {
MessageInfo? lookup(MessageCache cache); Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier);
void add(MessageCache cache, MessageInfo messageInfo, String contenthash);
} }
class ByIndex implements CacheHandler { class ByIndex implements CacheHandler {
@ -73,16 +71,51 @@ class ByIndex implements CacheHandler {
ByIndex(this.index); ByIndex(this.index);
MessageInfo? lookup(MessageCache cache) { Future<MessageInfo?> lookup(MessageCache cache) async {
var msg = cache.getByIndex(index);
return msg;
}
Future<MessageInfo?> get( Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
// if in cache, get
if (index < cache.cacheByIndex.length) {
return cache.getByIndex(index);
}
// otherwise we are going to fetch, so we'll fetch a chunk of messages
// observationally flutter future builder seemed to be reaching for 20-40 message on pane load, so we start trying to load up to that many messages in one request
var chunk = 40;
// check that we aren't asking for messages beyond stored messages
if (index + chunk >= cache.storageMessageCount) {
chunk = cache.storageMessageCount - index;
if (chunk <= 0) {
return Future.value(null);
}
}
cache.lockIndexes(index, index+chunk);
var msgs = await cwtch.GetMessages(profileOnion, conversationIdentifier, index, chunk);
int i = 0; // i used to loop through returned messages. if doesn't reach the requested count, we will use it in the finally stanza to error out the remaining asked for messages in the cache
try {
List<dynamic> messagesWrapper = jsonDecode(msgs);
for(; i < messagesWrapper.length; i++) {
var messageInfo = messageWrapperToInfo(profileOnion, conversationIdentifier, messagesWrapper[i]);
cache.addIndexed(messageInfo, index + i);
}
//messageWrapperToInfo
} catch (e, stacktrace) {
EnvironmentConfig.debugLog("Error: Getting indexed messages $index to ${index+chunk} failed parsing: " + e.toString() + " " + stacktrace.toString());
} finally {
if (i != chunk) {
cache.malformIndexes(index+i, index+chunk);
}
}
return cache.getByIndex(index); return cache.getByIndex(index);
} }
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { void add(MessageCache cache, MessageInfo messageInfo) {
return cwtch.GetMessage(profileOnion, conversationIdentifier, index); cache.addIndexed(messageInfo, index);
}
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
cache.add(messageInfo, index, contenthash);
} }
} }
@ -91,17 +124,28 @@ class ById implements CacheHandler {
ById(this.id); ById(this.id);
MessageInfo? lookup(MessageCache cache) { Future<MessageInfo?> lookup(MessageCache cache) {
return cache.getById(id); return Future<MessageInfo?>.value(cache.getById(id));
} }
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
return cwtch.GetMessageByID(profileOnion, conversationIdentifier, id); var rawMessageEnvelope = await cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
if (messageInfo == null) {
return Future.value(null);
}
cache.addUnindexed(messageInfo);
return Future.value(messageInfo);
} }
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) { Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
cache.addUnindexed(messageInfo, contenthash); var messageInfo = await lookup(cache);
if (messageInfo != null) {
return Future.value(messageInfo);
}
return fetch(cwtch, profileOnion, conversationIdentifier, cache);
} }
} }
class ByContentHash implements CacheHandler { class ByContentHash implements CacheHandler {
@ -109,113 +153,91 @@ class ByContentHash implements CacheHandler {
ByContentHash(this.hash); ByContentHash(this.hash);
MessageInfo? lookup(MessageCache cache) { Future<MessageInfo?> lookup(MessageCache cache) {
return cache.getByContentHash(hash); return Future<MessageInfo?>.value(cache.getByContentHash(hash));
} }
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) { Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
return cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash); var rawMessageEnvelope = await cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
} var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
if (messageInfo == null) {
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) { return Future.value(null);
cache.addUnindexed(messageInfo, contenthash);
}
}
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) {
var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false);
// Hit cache
MessageInfo? messageInfo = getMessageInfoFromCache(context, profileOnion, conversationIdentifier, cacheHandler);
if (messageInfo != null) {
return Future.value(compileOverlay(messageInfo.metadata, messageInfo.wrapper));
}
// Fetch and Cache
var messageInfoFuture = fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
return messageInfoFuture.then((MessageInfo? messageInfo) {
if (messageInfo != null) {
return compileOverlay(messageInfo.metadata, messageInfo.wrapper);
} else {
return MalformedMessage(malformedMetadata);
} }
}); cache.addUnindexed(messageInfo);
return Future.value(messageInfo);
}
Future<MessageInfo?> get( Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
var messageInfo = await lookup(cache);
if (messageInfo != null) {
return Future.value(messageInfo);
}
return fetch(cwtch, profileOnion, conversationIdentifier, cache);
}
} }
MessageInfo? getMessageInfoFromCache(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) { Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
// Hit cache var malformedMetadata = MessageMetadata(profileOnion, conversationIdentifier, 0, DateTime.now(), "", "", "", <String, String>{}, false, true, false, "");
var cwtch = Provider.of<FlwtchState>(context, listen: false).cwtch;
MessageCache? cache;
try { try {
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache; cache = Provider
if (cache != null) { .of<ProfileInfoState>(context, listen: false)
MessageInfo? messageInfo = cacheHandler.lookup(cache); .contactList
if (messageInfo != null) { .getContact(conversationIdentifier)
return messageInfo; ?.messageCache;
} if (cache == null) {
EnvironmentConfig.debugLog("error: cannot get message cache for profile: $profileOnion conversation: $conversationIdentifier");
return MalformedMessage(malformedMetadata);
} }
} catch (e) { } catch (e) {
EnvironmentConfig.debugLog("message handler exception on get from cache: $e"); EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
// provider check failed...make an expensive call... // provider check failed...make an expensive call...
return MalformedMessage(malformedMetadata);
}
MessageInfo? messageInfo = await cacheHandler.get(cwtch, profileOnion, conversationIdentifier, cache);
if (messageInfo != null) {
return compileOverlay(messageInfo.metadata, messageInfo.wrapper);
} else {
return MalformedMessage(malformedMetadata);
} }
return null;
} }
Future<MessageInfo?> fetchAndCacheMessageInfo(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) { MessageInfo? messageJsonToInfo(String profileOnion, int conversationIdentifier, dynamic messageJson) {
// Load and cache
var profileInfostate = Provider.of<ProfileInfoState>(context, listen: false);
try { try {
Future<dynamic> rawMessageEnvelopeFuture; dynamic messageWrapper = jsonDecode(messageJson);
rawMessageEnvelopeFuture = cacheHandler.fetch(Provider.of<FlwtchState>(context, listen: false).cwtch, profileOnion, conversationIdentifier); if (messageWrapper == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return null;
}
return rawMessageEnvelopeFuture.then((dynamic rawMessageEnvelope) { return messageWrapperToInfo(profileOnion, conversationIdentifier, messageWrapper);
try { } catch (e, stacktrace) {
dynamic messageWrapper = jsonDecode(rawMessageEnvelope); EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
// There are 2 conditions in which this error condition can be met: return null;
// 1. The application == nil, in which case this instance of the UI is already
// broken beyond repair, and will either be replaced by a new version, or requires a complete
// restart.
// 2. This index was incremented and we happened to fetch the timeline prior to the messages inclusion.
// This should be rare as Timeline addition/fetching is mutex protected and Dart itself will pipeline the
// calls to libCwtch-go - however because we use goroutines on the backend there is always a chance that one
// will find itself delayed.
// The second case is recoverable by tail-recursing this future.
if (messageWrapper['Message'] == null || messageWrapper['Message'] == '' || messageWrapper['Message'] == '{}') {
return Future.delayed(Duration(seconds: 2), () {
print("Tail recursive call to messageHandler called. This should be a rare event. If you see multiples of this log over a short period of time please log it as a bug.");
return fetchAndCacheMessageInfo(context, profileOnion, conversationIdentifier, cacheHandler);
});
}
// Construct the initial metadata
var messageID = messageWrapper['ID'];
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage'];
var attributes = messageWrapper['Attributes'];
var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null;
var signature = messageWrapper['Signature'];
var contenthash = messageWrapper['ContentHash'];
var localIndex = messageWrapper['LocalIndex'];
var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false);
var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
var cache = profileInfostate.contactList.getContact(conversationIdentifier)?.messageCache;
if (cache != null) {
cacheHandler.add(cache, messageInfo, contenthash);
}
return messageInfo;
} catch (e, stacktrace) {
EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
return null;
}
});
} catch (e) {
EnvironmentConfig.debugLog("message handler exeption on get message: $e");
return Future.value(null);
} }
} }
MessageInfo messageWrapperToInfo(String profileOnion, int conversationIdentifier, dynamic messageWrapper) {
// Construct the initial metadata
var messageID = messageWrapper['ID'];
var timestamp = DateTime.tryParse(messageWrapper['Timestamp'])!;
var senderHandle = messageWrapper['PeerID'];
var senderImage = messageWrapper['ContactImage'];
var attributes = messageWrapper['Attributes'];
var ackd = messageWrapper['Acknowledged'];
var error = messageWrapper['Error'] != null;
var signature = messageWrapper['Signature'];
var contenthash = messageWrapper['ContentHash'];
var metadata = MessageMetadata(profileOnion, conversationIdentifier, messageID, timestamp, senderHandle, senderImage, signature, attributes, ackd, error, false, contenthash);
var messageInfo = new MessageInfo(metadata, messageWrapper['Message']);
return messageInfo;
}
class MessageMetadata extends ChangeNotifier { class MessageMetadata extends ChangeNotifier {
// meta-metadata // meta-metadata
final String profileOnion; final String profileOnion;
@ -231,6 +253,7 @@ class MessageMetadata extends ChangeNotifier {
final bool isAuto; final bool isAuto;
final String? signature; final String? signature;
final String contenthash;
dynamic get attributes => this._attributes; dynamic get attributes => this._attributes;
@ -249,5 +272,5 @@ class MessageMetadata extends ChangeNotifier {
} }
MessageMetadata( MessageMetadata(
this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error, this.isAuto); this.profileOnion, this.conversationIdentifier, this.messageID, this.timestamp, this.senderHandle, this.senderImage, this.signature, this._attributes, this._ackd, this._error, this.isAuto, this.contenthash);
} }

View File

@ -1,58 +1,148 @@
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'message.dart'; import 'message.dart';
class MessageInfo { class MessageInfo {
final MessageMetadata metadata; late MessageMetadata metadata;
final String wrapper; late String wrapper;
MessageInfo(this.metadata, this.wrapper); MessageInfo(this.metadata, this.wrapper);
} }
class LocalIndexMessage {
late bool cacheOnly;
late bool isLoading;
late Future<void> loaded;
late Completer<void> loader;
late int? messageId;
LocalIndexMessage(int? messageId, {cacheOnly = false, isLoading = false}) {
this.messageId = messageId;
this.cacheOnly = cacheOnly;
this.isLoading = isLoading;
if (isLoading) {
loader = Completer<void>();
loaded = loader.future;
}
}
void finishLoad(int messageId) {
this.messageId = messageId;
isLoading = false;
loader.complete(true);
}
void failLoad() {
this.messageId = null;
isLoading = false;
loader.complete(true);
}
Future<void> waitForLoad() {
return loaded;
}
Future<int?> get() async {
if (isLoading) {
await waitForLoad();
}
return messageId;
}
}
// Message cache stores messages for use by the UI and uses MessageHandler and associated ByX loaders
// the cache stores messages in a cache indexed by their storage Id, and has two secondary indexes into it, content hash, and local index
// Index is the primary way to access the cache as it is a sequential ordered access and is used by the message pane
// contentHash is used for fetching replies
// by Id is used when composing a reply
// cacheByIndex supports additional features than just a direct index into the cache (byID)
// it allows locking of ranges in order to support bulk sequential loading (see ByIndex in message.dart)
// cacheByIndex allows allows inserting temporarily non storage backed messages so that Send Message can be respected instantly and then updated upon insertion into backend
// the message cache needs storageMessageCount maintained by the system so it can inform bulk loading when it's reaching the end of fetchable messages
class MessageCache extends ChangeNotifier { class MessageCache extends ChangeNotifier {
// cache of MessageId to Message
late Map<int, MessageInfo> cache; late Map<int, MessageInfo> cache;
late List<int?> cacheByIndex;
// local index to MessageId
late List<LocalIndexMessage> cacheByIndex;
// map of content hash to MessageId
late Map<String, int> cacheByHash; late Map<String, int> cacheByHash;
MessageCache() { late int _storageMessageCount;
MessageCache(int storageMessageCount) {
cache = {}; cache = {};
cacheByIndex = List.empty(growable: true); cacheByIndex = List.empty(growable: true);
cacheByHash = {}; cacheByHash = {};
this._storageMessageCount = storageMessageCount;
} }
int get indexedLength => cacheByIndex.length; int get indexedLength => cacheByIndex.length;
int get storageMessageCount => _storageMessageCount;
set storageMessageCount(int newval) {
this._storageMessageCount = newval;
}
MessageInfo? getById(int id) => cache[id]; MessageInfo? getById(int id) => cache[id];
MessageInfo? getByIndex(int index) {
Future<MessageInfo?> getByIndex(int index) async {
if (index >= cacheByIndex.length) { if (index >= cacheByIndex.length) {
return null; return null;
} }
return cache[cacheByIndex[index]]; var id = await cacheByIndex[index].get();
if (id == null) {
return Future<MessageInfo?>.value(null);
}
return cache[id];
} }
MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]]; MessageInfo? getByContentHash(String contenthash) => cache[cacheByHash[contenthash]];
void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash) { void addNew(String profileOnion, int conversation, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash) {
this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto), data); this.cache[messageID] = MessageInfo(MessageMetadata(profileOnion, conversation, messageID, timestamp, senderHandle, senderImage, "", {}, false, false, isAuto, contenthash), data);
this.cacheByIndex.insert(0, messageID); this.cacheByIndex.insert(0, LocalIndexMessage(messageID));
if (contenthash != null && contenthash != "") { if (contenthash != null && contenthash != "") {
this.cacheByHash[contenthash] = messageID; this.cacheByHash[contenthash] = messageID;
} }
notifyListeners(); notifyListeners();
} }
void add(MessageInfo messageInfo, int index, String? contenthash) { // inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents
this.cache[messageInfo.metadata.messageID] = messageInfo; // or .failLoad() is called on them to mark them malformed
this.cacheByIndex.insert(index, messageInfo.metadata.messageID); // this prevents successive ui message build requests from triggering multiple GetMesssage requests to the backend, as the first one locks a block of messages and the rest wait on that
if (contenthash != null && contenthash != "") { void lockIndexes(int start, int end) {
this.cacheByHash[contenthash] = messageInfo.metadata.messageID; for(var i = start; i < end; i++) {
this.cacheByIndex.insert(i, LocalIndexMessage(null, isLoading: true));
} }
}
void malformIndexes(int start, int end) {
for(var i = start; i < end; i++) {
this.cacheByIndex[i].failLoad();
}
}
void addIndexed(MessageInfo messageInfo, int index) {
this.cache[messageInfo.metadata.messageID] = messageInfo;
if (index < this.cacheByIndex.length ) {
this.cacheByIndex[index].finishLoad(messageInfo.metadata.messageID);
} else {
this.cacheByIndex.insert(index, LocalIndexMessage(messageInfo.metadata.messageID));
}
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
notifyListeners(); notifyListeners();
} }
void addUnindexed(MessageInfo messageInfo, String? contenthash) { void addUnindexed(MessageInfo messageInfo) {
this.cache[messageInfo.metadata.messageID] = messageInfo; this.cache[messageInfo.metadata.messageID] = messageInfo;
if (contenthash != null && contenthash != "") { if (messageInfo.metadata.contenthash != "") {
this.cacheByHash[contenthash] = messageInfo.metadata.messageID; this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
} }
notifyListeners(); notifyListeners();
} }

View File

@ -202,7 +202,7 @@ class ProfileInfoState extends ChangeNotifier {
} }
void newMessage( void newMessage(
int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String? contenthash, bool selectedProfile, bool selectedConversation) { int identifier, int messageID, DateTime timestamp, String senderHandle, String senderImage, bool isAuto, String data, String contenthash, bool selectedProfile, bool selectedConversation) {
if (!selectedProfile) { if (!selectedProfile) {
unreadMessages++; unreadMessages++;
notifyListeners(); notifyListeners();

View File

@ -229,17 +229,15 @@ class _MessageViewState extends State<MessageView> {
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage); ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm)).then(_sendMessageHandler);
} catch (e) {} } catch (e) {}
Provider.of<AppState>(context, listen: false).selectedIndex = null; Provider.of<AppState>(context, listen: false).selectedIndex = null;
_sendMessageHelper();
}); });
} else { } else {
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm)); .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm)).then(_sendMessageHandler);
_sendMessageHelper();
} }
} }
} }
@ -247,29 +245,40 @@ class _MessageViewState extends State<MessageView> {
void _sendInvitation([String? ignoredParam]) { void _sendInvitation([String? ignoredParam]) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact); .SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact).then(_sendMessageHandler);
_sendMessageHelper();
} }
void _sendFile(String filePath) { void _sendFile(String filePath) {
Provider.of<FlwtchState>(context, listen: false) Provider.of<FlwtchState>(context, listen: false)
.cwtch .cwtch
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath); .ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath).then(_sendMessageHandler);
_sendMessageHelper();
} }
void _sendMessageHelper() { void _sendMessageHandler(dynamic messageJson) {
var cache = Provider.of<ContactInfoState>(context, listen: false).messageCache;
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
var profile = Provider.of<ProfileInfoState>(context, listen: false);
var messageInfo = messageJsonToInfo(profileOnion, identifier, messageJson);
if (messageInfo != null) {
profile.newMessage(
messageInfo.metadata.conversationIdentifier,
messageInfo.metadata.messageID,
messageInfo.metadata.timestamp,
messageInfo.metadata.senderHandle,
messageInfo.metadata.senderImage ?? "",
messageInfo.metadata.isAuto,
messageInfo.wrapper,
messageInfo.metadata.contenthash,
true,
true,
);
}
ctrlrCompose.clear(); ctrlrCompose.clear();
focusNode.requestFocus(); focusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 80), () {
var profile = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
var identifier = Provider.of<ContactInfoState>(context, listen: false).identifier;
fetchAndCacheMessageInfo(context, profile, identifier, ByIndex(0));
Provider.of<ContactInfoState>(context, listen: false).newMarker++;
Provider.of<ContactInfoState>(context, listen: false).totalMessages += 1;
// Resort the contact list...
Provider.of<ProfileInfoState>(context, listen: false).contactList.updateLastMessageTime(Provider.of<ContactInfoState>(context, listen: false).identifier, DateTime.now());
});
} }
Widget _buildComposeBox() { Widget _buildComposeBox() {

View File

@ -223,10 +223,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: widgetRow, children: widgetRow,
))))); )))));
var mark = Provider.of<ContactInfoState>(context).newMarker;
if (mark > 0 && var markMsgId = Provider.of<ContactInfoState>(context).newMarkerMsgId;
Provider.of<ContactInfoState>(context).messageCache.indexedLength > mark && if (markMsgId == Provider.of<MessageMetadata>(context).messageID) {
Provider.of<ContactInfoState>(context).messageCache.getByIndex(mark - 1)?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]); return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
} else { } else {
return mr; return mr;