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
|
@ -1 +1 @@
|
|||
2022-03-22-17-15-v1.6.0-11-gca4897b
|
||||
2022-03-23-19-06-v1.6.0-13-g1acae32
|
||||
|
|
|
@ -1 +1 @@
|
|||
2022-03-22-21-16-v1.6.0-11-gca4897b
|
||||
2022-03-23-23-06-v1.6.0-13-g1acae32
|
||||
|
|
|
@ -290,25 +290,7 @@ class FlwtchWorker(context: Context, parameters: WorkerParameters) :
|
|||
val conversation = a.getInt("conversation").toLong()
|
||||
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" -> {
|
||||
val profile = (a.get("ProfileOnion") as? String) ?: ""
|
||||
val conversation = a.getInt("conversation").toLong()
|
||||
|
|
|
@ -35,6 +35,9 @@ import android.os.Environment
|
|||
import android.database.Cursor
|
||||
import android.provider.MediaStore
|
||||
|
||||
import cwtch.Cwtch
|
||||
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
override fun provideSplashScreen(): SplashScreen? = SplashView()
|
||||
|
||||
|
@ -168,84 +171,117 @@ class MainActivity: FlutterActivity() {
|
|||
// receives messages from the ForegroundService (which provides, ironically enough, the backend)
|
||||
private fun handleCwtch(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||
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>
|
||||
|
||||
// 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
|
||||
// the eventbus listener.
|
||||
if (call.method == "Start") {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
when (call.method) {
|
||||
"Start" -> {
|
||||
val uniqueTag = argmap["torPath"] ?: "nullEventBus"
|
||||
|
||||
// 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
|
||||
// that we can divert this method call to ReconnectCwtchForeground instead if so.
|
||||
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
|
||||
for (workInfo in works) {
|
||||
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"))
|
||||
// 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
|
||||
// that we can divert this method call to ReconnectCwtchForeground instead if so.
|
||||
val works = WorkManager.getInstance(this).getWorkInfosByTag(WORKER_TAG).get()
|
||||
for (workInfo in works) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
"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
|
||||
|
|
|
@ -48,12 +48,15 @@ abstract class Cwtch {
|
|||
Future<dynamic> GetMessageByContentHash(String profile, int handle, String contentHash);
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profile, int handle, String message);
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profile, int handle, int target);
|
||||
Future<dynamic> GetMessages(String profile, int handle, int index, int count);
|
||||
|
||||
// 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
|
||||
void DownloadFile(String profile, int handle, String filepath, String manifestpath, String filekey);
|
||||
// android-only
|
||||
|
|
|
@ -159,7 +159,7 @@ class CwtchNotifier {
|
|||
var senderHandle = data['RemotePeer'];
|
||||
var senderImage = data['picture'];
|
||||
var isAuto = data['Auto'] == "true";
|
||||
String? contenthash = data['ContentHash'];
|
||||
String contenthash = data['ContentHash'];
|
||||
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
||||
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
|
||||
var notification = data["notification"];
|
||||
|
@ -211,7 +211,7 @@ class CwtchNotifier {
|
|||
var contact = profileCN.getProfile(data["ProfileOnion"])?.contactList.getContact(identifier);
|
||||
var currentTotal = contact!.totalMessages;
|
||||
var isAuto = data['Auto'] == "true";
|
||||
String? contenthash = data['ContentHash'];
|
||||
String contenthash = data['ContentHash'];
|
||||
var selectedProfile = appState.selectedProfile == data["ProfileOnion"];
|
||||
var selectedConversation = selectedProfile && appState.selectedConversation == identifier;
|
||||
var notification = data["notification"];
|
||||
|
|
|
@ -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 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 {
|
||||
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);
|
||||
|
@ -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 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 GetJsonBlobFromStrIntStringFn = Pointer<Utf8> Function(
|
||||
Pointer<Utf8>,
|
||||
|
@ -300,6 +306,19 @@ class CwtchFfi implements Cwtch {
|
|||
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
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendProfileEvent(String onion, String json) {
|
||||
|
@ -359,39 +378,48 @@ class CwtchFfi implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profileOnion, int contactHandle, String message) {
|
||||
var sendMessage = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_SendMessage");
|
||||
Future<dynamic> SendMessage(String profileOnion, int contactHandle, String message) async {
|
||||
var sendMessage = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_SendMessage");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SendMessage = sendMessage.asFunction<VoidFromStringIntStringFn>();
|
||||
final SendMessage = sendMessage.asFunction<GetJsonBlobFromStrIntStrFn>();
|
||||
final u1 = profileOnion.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(u3);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profileOnion, int contactHandle, int target) {
|
||||
var sendInvitation = library.lookup<NativeFunction<void_from_string_int_int_function>>("c_SendInvitation");
|
||||
Future<dynamic> SendInvitation(String profileOnion, int contactHandle, int target) async {
|
||||
var sendInvitation = library.lookup<NativeFunction<get_json_blob_from_str_int_int_function>>("c_SendInvitation");
|
||||
// ignore: non_constant_identifier_names
|
||||
final SendInvitation = sendInvitation.asFunction<VoidFromStringIntIntFn>();
|
||||
final SendInvitation = sendInvitation.asFunction<GetJsonBlobFromStrIntIntFn>();
|
||||
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);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, int contactHandle, String filepath) {
|
||||
var shareFile = library.lookup<NativeFunction<void_from_string_int_string_function>>("c_ShareFile");
|
||||
Future<dynamic> ShareFile(String profileOnion, int contactHandle, String filepath) async {
|
||||
var shareFile = library.lookup<NativeFunction<get_json_blob_from_string_int_string_function>>("c_ShareFile");
|
||||
// ignore: non_constant_identifier_names
|
||||
final ShareFile = shareFile.asFunction<VoidFromStringIntStringFn>();
|
||||
final ShareFile = shareFile.asFunction<GetJsonBlobFromStrIntStrFn>();
|
||||
final u1 = profileOnion.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(u3);
|
||||
return jsonMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -94,6 +94,11 @@ class CwtchGomobile implements Cwtch {
|
|||
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
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendProfileEvent(String onion, String jsonEvent) {
|
||||
|
@ -129,20 +134,20 @@ class CwtchGomobile implements Cwtch {
|
|||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendMessage(String profileOnion, int conversation, String message) {
|
||||
cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
|
||||
Future<dynamic> SendMessage(String profileOnion, int conversation, String message) {
|
||||
return cwtchPlatform.invokeMethod("SendMessage", {"ProfileOnion": profileOnion, "conversation": conversation, "message": message});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void SendInvitation(String profileOnion, int conversation, int target) {
|
||||
cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
|
||||
Future<dynamic> SendInvitation(String profileOnion, int conversation, int target) {
|
||||
return cwtchPlatform.invokeMethod("SendInvitation", {"ProfileOnion": profileOnion, "conversation": conversation, "target": target});
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: non_constant_identifier_names
|
||||
void ShareFile(String profileOnion, int conversation, String filepath) {
|
||||
cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
|
||||
Future<dynamic> ShareFile(String profileOnion, int conversation, String filepath) {
|
||||
return cwtchPlatform.invokeMethod("ShareFile", {"ProfileOnion": profileOnion, "conversation": conversation, "filepath": filepath});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -42,7 +42,7 @@ class ContactInfoState extends ChangeNotifier {
|
|||
late int _totalMessages = 0;
|
||||
late DateTime _lastMessageTime;
|
||||
late Map<String, GlobalKey<MessageRowState>> keys;
|
||||
int _newMarker = 0;
|
||||
int _newMarkerMsgId = -1;
|
||||
DateTime _newMarkerClearAt = DateTime.now();
|
||||
late MessageCache messageCache;
|
||||
|
||||
|
@ -82,7 +82,7 @@ class ContactInfoState extends ChangeNotifier {
|
|||
this._server = server;
|
||||
this._archived = archived;
|
||||
this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
|
||||
this.messageCache = new MessageCache();
|
||||
this.messageCache = new MessageCache(_totalMessages);
|
||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||
}
|
||||
|
||||
|
@ -148,41 +148,29 @@ class ContactInfoState extends ChangeNotifier {
|
|||
int get unreadMessages => this._unreadMessages;
|
||||
|
||||
set unreadMessages(int newVal) {
|
||||
// don't reset newMarker position when unreadMessages is being cleared
|
||||
if (newVal > 0) {
|
||||
this._newMarker = newVal;
|
||||
} else {
|
||||
if (newVal == 0) {
|
||||
// conversation has been selected, start the countdown for the New Messager marker to be reset
|
||||
this._newMarkerClearAt = DateTime.now().add(const Duration(minutes: 2));
|
||||
}
|
||||
this._unreadMessages = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get newMarker {
|
||||
int get newMarkerMsgId {
|
||||
if (DateTime.now().isAfter(this._newMarkerClearAt)) {
|
||||
// perform heresy
|
||||
this._newMarker = 0;
|
||||
this._newMarkerMsgId = -1;
|
||||
// no need to notifyListeners() because presumably this getter is
|
||||
// being called from a renderer anyway
|
||||
}
|
||||
return this._newMarker;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
return this._newMarkerMsgId;
|
||||
}
|
||||
|
||||
int get totalMessages => this._totalMessages;
|
||||
|
||||
set totalMessages(int newVal) {
|
||||
this._totalMessages = newVal;
|
||||
this.messageCache.storageMessageCount = newVal;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -251,11 +239,12 @@ class ContactInfoState extends ChangeNotifier {
|
|||
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) {
|
||||
unreadMessages++;
|
||||
} else {
|
||||
newMarker++;
|
||||
}
|
||||
if (_newMarkerMsgId == -1) {
|
||||
_newMarkerMsgId = messageID;
|
||||
}
|
||||
|
||||
this._lastMessageTime = timestamp;
|
||||
|
|
|
@ -123,7 +123,7 @@ class ContactListState extends ChangeNotifier {
|
|||
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);
|
||||
updateLastMessageTime(identifier, DateTime.now());
|
||||
}
|
||||
|
|
|
@ -63,9 +63,7 @@ Message compileOverlay(MessageMetadata metadata, String messageData) {
|
|||
}
|
||||
|
||||
abstract class CacheHandler {
|
||||
MessageInfo? lookup(MessageCache cache);
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier);
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash);
|
||||
Future<MessageInfo?> get(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache);
|
||||
}
|
||||
|
||||
class ByIndex implements CacheHandler {
|
||||
|
@ -73,16 +71,51 @@ class ByIndex implements CacheHandler {
|
|||
|
||||
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;
|
||||
dan marked this conversation as resolved
|
||||
// check that we aren't asking for messages beyond stored messages
|
||||
if (index + chunk >= cache.storageMessageCount) {
|
||||
chunk = cache.storageMessageCount - index;
|
||||
dan marked this conversation as resolved
sarah
commented
lockIndexes lockIndex*e*s
|
||||
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);
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessage(profileOnion, conversationIdentifier, index);
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||
cache.add(messageInfo, index, contenthash);
|
||||
void add(MessageCache cache, MessageInfo messageInfo) {
|
||||
cache.addIndexed(messageInfo, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,17 +124,28 @@ class ById implements CacheHandler {
|
|||
|
||||
ById(this.id);
|
||||
|
||||
MessageInfo? lookup(MessageCache cache) {
|
||||
return cache.getById(id);
|
||||
Future<MessageInfo?> lookup(MessageCache cache) {
|
||||
return Future<MessageInfo?>.value(cache.getById(id));
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessageByID(profileOnion, conversationIdentifier, id);
|
||||
Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
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) {
|
||||
cache.addUnindexed(messageInfo, contenthash);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ByContentHash implements CacheHandler {
|
||||
|
@ -109,113 +153,91 @@ class ByContentHash implements CacheHandler {
|
|||
|
||||
ByContentHash(this.hash);
|
||||
|
||||
MessageInfo? lookup(MessageCache cache) {
|
||||
return cache.getByContentHash(hash);
|
||||
Future<MessageInfo?> lookup(MessageCache cache) {
|
||||
return Future<MessageInfo?>.value(cache.getByContentHash(hash));
|
||||
}
|
||||
|
||||
Future<dynamic> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier) {
|
||||
return cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
|
||||
}
|
||||
|
||||
void add(MessageCache cache, MessageInfo messageInfo, String contenthash) {
|
||||
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);
|
||||
Future<MessageInfo?> fetch(Cwtch cwtch, String profileOnion, int conversationIdentifier, MessageCache cache) async {
|
||||
var rawMessageEnvelope = await cwtch.GetMessageByContentHash(profileOnion, conversationIdentifier, hash);
|
||||
var messageInfo = messageJsonToInfo(profileOnion, conversationIdentifier, rawMessageEnvelope);
|
||||
if (messageInfo == null) {
|
||||
return Future.value(null);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
// Hit cache
|
||||
Future<Message> messageHandler(BuildContext context, String profileOnion, int conversationIdentifier, CacheHandler cacheHandler) async {
|
||||
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 {
|
||||
var cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationIdentifier)?.messageCache;
|
||||
if (cache != null) {
|
||||
MessageInfo? messageInfo = cacheHandler.lookup(cache);
|
||||
if (messageInfo != null) {
|
||||
return messageInfo;
|
||||
}
|
||||
cache = Provider
|
||||
.of<ProfileInfoState>(context, listen: false)
|
||||
.contactList
|
||||
.getContact(conversationIdentifier)
|
||||
?.messageCache;
|
||||
if (cache == null) {
|
||||
EnvironmentConfig.debugLog("error: cannot get message cache for profile: $profileOnion conversation: $conversationIdentifier");
|
||||
return MalformedMessage(malformedMetadata);
|
||||
}
|
||||
} catch (e) {
|
||||
EnvironmentConfig.debugLog("message handler exception on get from cache: $e");
|
||||
// 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) {
|
||||
// Load and cache
|
||||
var profileInfostate = Provider.of<ProfileInfoState>(context, listen: false);
|
||||
MessageInfo? messageJsonToInfo(String profileOnion, int conversationIdentifier, dynamic messageJson) {
|
||||
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) {
|
||||
try {
|
||||
dynamic messageWrapper = jsonDecode(rawMessageEnvelope);
|
||||
// There are 2 conditions in which this error condition can be met:
|
||||
// 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);
|
||||
return messageWrapperToInfo(profileOnion, conversationIdentifier, messageWrapper);
|
||||
} catch (e, stacktrace) {
|
||||
EnvironmentConfig.debugLog("message handler exception on parse message and cache: " + e.toString() + " " + stacktrace.toString());
|
||||
return 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 {
|
||||
// meta-metadata
|
||||
final String profileOnion;
|
||||
|
@ -231,6 +253,7 @@ class MessageMetadata extends ChangeNotifier {
|
|||
final bool isAuto;
|
||||
|
||||
final String? signature;
|
||||
final String contenthash;
|
||||
|
||||
dynamic get attributes => this._attributes;
|
||||
|
||||
|
@ -249,5 +272,5 @@ class MessageMetadata extends ChangeNotifier {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,58 +1,148 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'message.dart';
|
||||
|
||||
class MessageInfo {
|
||||
final MessageMetadata metadata;
|
||||
final String wrapper;
|
||||
late MessageMetadata metadata;
|
||||
late String 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 {
|
||||
// cache of MessageId to Message
|
||||
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;
|
||||
|
||||
MessageCache() {
|
||||
late int _storageMessageCount;
|
||||
|
||||
MessageCache(int storageMessageCount) {
|
||||
cache = {};
|
||||
cacheByIndex = List.empty(growable: true);
|
||||
cacheByHash = {};
|
||||
this._storageMessageCount = storageMessageCount;
|
||||
}
|
||||
|
||||
int get indexedLength => cacheByIndex.length;
|
||||
|
||||
int get storageMessageCount => _storageMessageCount;
|
||||
set storageMessageCount(int newval) {
|
||||
this._storageMessageCount = newval;
|
||||
}
|
||||
|
||||
MessageInfo? getById(int id) => cache[id];
|
||||
MessageInfo? getByIndex(int index) {
|
||||
|
||||
Future<MessageInfo?> getByIndex(int index) async {
|
||||
if (index >= cacheByIndex.length) {
|
||||
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]];
|
||||
|
||||
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.cacheByIndex.insert(0, messageID);
|
||||
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, contenthash), data);
|
||||
this.cacheByIndex.insert(0, LocalIndexMessage(messageID));
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageID;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void add(MessageInfo messageInfo, int index, String? contenthash) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
this.cacheByIndex.insert(index, messageInfo.metadata.messageID);
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||
// inserts place holder values into the index cache that will block on .get() until .finishLoad() is called on them with message contents
|
||||
// or .failLoad() is called on them to mark them malformed
|
||||
// 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
|
||||
void lockIndexes(int start, int end) {
|
||||
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();
|
||||
}
|
||||
|
||||
void addUnindexed(MessageInfo messageInfo, String? contenthash) {
|
||||
void addUnindexed(MessageInfo messageInfo) {
|
||||
this.cache[messageInfo.metadata.messageID] = messageInfo;
|
||||
if (contenthash != null && contenthash != "") {
|
||||
this.cacheByHash[contenthash] = messageInfo.metadata.messageID;
|
||||
if (messageInfo.metadata.contenthash != "") {
|
||||
this.cacheByHash[messageInfo.metadata.contenthash] = messageInfo.metadata.messageID;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@ class ProfileInfoState extends ChangeNotifier {
|
|||
}
|
||||
|
||||
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) {
|
||||
unreadMessages++;
|
||||
notifyListeners();
|
||||
|
|
|
@ -229,17 +229,15 @@ class _MessageViewState extends State<MessageView> {
|
|||
ChatMessage cm = new ChatMessage(o: QuotedMessageOverlay, d: quotedMessage);
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.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) {}
|
||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
||||
_sendMessageHelper();
|
||||
});
|
||||
} else {
|
||||
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm));
|
||||
_sendMessageHelper();
|
||||
.SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, jsonEncode(cm)).then(_sendMessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,29 +245,40 @@ class _MessageViewState extends State<MessageView> {
|
|||
void _sendInvitation([String? ignoredParam]) {
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact);
|
||||
_sendMessageHelper();
|
||||
.SendInvitation(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, this.selectedContact).then(_sendMessageHandler);
|
||||
}
|
||||
|
||||
void _sendFile(String filePath) {
|
||||
|
||||
Provider.of<FlwtchState>(context, listen: false)
|
||||
.cwtch
|
||||
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath);
|
||||
_sendMessageHelper();
|
||||
.ShareFile(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).identifier, filePath).then(_sendMessageHandler);
|
||||
}
|
||||
|
||||
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();
|
||||
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() {
|
||||
|
|
|
@ -223,10 +223,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: widgetRow,
|
||||
)))));
|
||||
var mark = Provider.of<ContactInfoState>(context).newMarker;
|
||||
if (mark > 0 &&
|
||||
Provider.of<ContactInfoState>(context).messageCache.indexedLength > mark &&
|
||||
Provider.of<ContactInfoState>(context).messageCache.getByIndex(mark - 1)?.metadata.messageID == Provider.of<MessageMetadata>(context).messageID) {
|
||||
|
||||
var markMsgId = Provider.of<ContactInfoState>(context).newMarkerMsgId;
|
||||
if (markMsgId == Provider.of<MessageMetadata>(context).messageID) {
|
||||
return Column(crossAxisAlignment: fromMe ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [Align(alignment: Alignment.center, child: _bubbleNew()), mr]);
|
||||
} else {
|
||||
return mr;
|
||||
|
|
Loading…
Reference in New Issue
what is this check for?
if it's in cache, get it
otherwise fetch follow logic follows, prolly should move this to the top