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
continuous-integration/drone/push Build is passing
Details
Reviewed-on: #407 Reviewed-by: Sarah Jamie Lewis <sarah@openprivacy.ca>
This commit is contained in:
commit
b8c1c7682b
|
@ -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()
|
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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"];
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue