Guard Android PM history and pending deletion across asynchronous sends
This commit is contained in:
1 parent
5778c8aa0a
commit
f83033d3a1
5 files changed
+61
-25
No files matched your search
@@ -336,7 +336,7 @@ object NativeLib {
|
||||
fun dmDelete(convId: String, generation: Long, request: Long): Boolean = nativeDmDelete(convId, generation, request)
|
||||
fun dmRequestConversations() { nativeDmRequestConversations() }
|
||||
fun dmRequestCallRoute(convId: String) { nativeDmRequestCallRoute(convId) }
|
||||
fun dmRequestMessages(convId: String) { nativeDmRequestMessages(convId) }
|
||||
fun dmRequestMessages(convId: String, generation: Long) { nativeDmRequestMessages(convId, generation) }
|
||||
fun dmSendFile(convId: String, path: String, name: String, generation: Long): Boolean = nativeDmSendFile(convId, path, name, generation)
|
||||
|
||||
private external fun nativeInit(dbPath: String, controlPort: Int, config: Any, logCb: Any): Boolean
|
||||
@@ -472,6 +472,6 @@ object NativeLib {
|
||||
private external fun nativeIsChannelMember(channelId: String, nodeId: Long): Boolean
|
||||
private external fun nativeDmRequestConversations()
|
||||
private external fun nativeDmRequestCallRoute(convId: String)
|
||||
private external fun nativeDmRequestMessages(convId: String)
|
||||
private external fun nativeDmRequestMessages(convId: String, generation: Long)
|
||||
private external fun nativeDmSendFile(convId: String, path: String, name: String, generation: Long): Boolean
|
||||
}
|
||||
+2
-1
@@ -42,7 +42,8 @@ fun ConversationComposer(target: ConversationTarget, viewModel: ChatViewModel,
|
||||
val reply = if (target is ConversationTarget.Channel) replies[target.id] else null
|
||||
val sendFailed by viewModel.messageSendFailed.collectAsState()
|
||||
val submitting by viewModel.submittingChannels.collectAsState()
|
||||
val pending = target is ConversationTarget.Channel && target.id in submitting
|
||||
val deletingDm by viewModel.dmDeleting.collectAsState()
|
||||
val pending = if (target is ConversationTarget.Channel) target.id in submitting else target.id in deletingDm
|
||||
if (sendFailed) AlertDialog(onDismissRequest = viewModel::dismissMessageSendError,
|
||||
text = { Text(stringResource(R.string.message_send_failed)) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissMessageSendError) { Text(stringResource(R.string.close)) } })
|
||||
|
||||
@@ -807,13 +807,19 @@ class ChatViewModel : ViewModel() {
|
||||
val cl = data[0].toInt() and 0xFF
|
||||
if (data.size != cl + 9) { LogManager.addLog("ERROR", "PM", "Invalid conversation update bytes=${data.size}"); return }
|
||||
val convId = String(data, 1, cl)
|
||||
val generation = java.nio.ByteBuffer.wrap(data, 1 + cl, 8).order(java.nio.ByteOrder.LITTLE_ENDIAN).long
|
||||
LogManager.addLog("INFO", "VM", "DM_CONV_UPDATED conv=$convId")
|
||||
refreshDmConversations()
|
||||
_currentDm.value?.let { if (it.convId == convId) refreshDmMessages() }
|
||||
_currentDm.value?.let {
|
||||
if (it.convId == convId && generation >= it.generation) {
|
||||
if (generation != it.generation) _dmMessages.value = emptyList()
|
||||
_currentDm.value = it.copy(generation = generation)
|
||||
refreshDmMessages()
|
||||
}
|
||||
}
|
||||
val pend = pendingStartPeer
|
||||
if (pend != null) {
|
||||
pendingStartPeer = null
|
||||
val generation = java.nio.ByteBuffer.wrap(data, 1 + cl, 8).order(java.nio.ByteOrder.LITTLE_ENDIAN).long
|
||||
_currentDm.value = DmConversation(convId = convId, peerNodeId = pend.first, peerName = pend.second,
|
||||
generation = generation)
|
||||
_dmCallAvailable.value = false
|
||||
@@ -835,14 +841,19 @@ class ChatViewModel : ViewModel() {
|
||||
if (cur != null && cur.convId == convId) refreshDmMessages()
|
||||
refreshDmConversations()
|
||||
}
|
||||
41 -> { /* DM_MESSAGES: [conv_len:1][conv:utf8][JSON:utf8] */
|
||||
41 -> { /* DM_MESSAGES: [conv_len:1][conv:utf8][generation:8][JSON:utf8] */
|
||||
if (data == null || data.isEmpty()) return
|
||||
val length = data[0].toInt() and 0xFF
|
||||
if (data.size <= 1 + length) return
|
||||
if (length == 0 || data.size <= 9 + length) {
|
||||
LogManager.addLog("ERROR", "PM", "Invalid history snapshot bytes=${data.size}"); return
|
||||
}
|
||||
val dm = _currentDm.value ?: return
|
||||
val conv = String(data, 1, length, Charsets.UTF_8)
|
||||
if (conv != dm.convId) return
|
||||
val json = String(data, 1 + length, data.size - 1 - length, Charsets.UTF_8)
|
||||
val generation = java.nio.ByteBuffer.wrap(data, 1 + length, 8).order(java.nio.ByteOrder.LITTLE_ENDIAN).long
|
||||
if (conv != dm.convId || generation != dm.generation || conv in _dmDeleting.value) {
|
||||
LogManager.addLog("DEBUG", "PM", "Discarded stale history conv=$conv generation=$generation"); return
|
||||
}
|
||||
val json = String(data, 9 + length, data.size - 9 - length, Charsets.UTF_8)
|
||||
_dmMessages.value = repo?.parseDmMessages(json, dm.peerName) ?: emptyList()
|
||||
}
|
||||
45 -> {
|
||||
@@ -852,7 +863,10 @@ class ChatViewModel : ViewModel() {
|
||||
it.copy(online = previous[it.convId]?.online == true)
|
||||
}
|
||||
_currentDm.value?.let { current ->
|
||||
_dmConversations.value.find { it.convId == current.convId }?.let { _currentDm.value = it }
|
||||
_dmConversations.value.find { it.convId == current.convId }?.let {
|
||||
_currentDm.value = it
|
||||
if (it.generation != current.generation) { _dmMessages.value = emptyList(); refreshDmMessages() }
|
||||
}
|
||||
}
|
||||
refreshDmCallRoute()
|
||||
}
|
||||
@@ -1212,7 +1226,7 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
fun refreshDmMessages() {
|
||||
val dm = _currentDm.value ?: return
|
||||
if (repo != null) NativeLib.dmRequestMessages(dm.convId)
|
||||
if (repo != null && dm.convId !in _dmDeleting.value) NativeLib.dmRequestMessages(dm.convId, dm.generation)
|
||||
}
|
||||
|
||||
fun startDm(channelId: String, nodeId: Long, name: String) {
|
||||
@@ -1241,13 +1255,18 @@ class ChatViewModel : ViewModel() {
|
||||
_dmMessages.value = emptyList()
|
||||
}
|
||||
|
||||
fun sendDmText(target: ConversationTarget.Pm, text: String): Boolean {
|
||||
val r = repo ?: return false
|
||||
if (text.isBlank()) return false
|
||||
/** Единая проверка PM до записи, копирования и native-публикации; ядро повторяет проверку поколения. */
|
||||
private fun dmTargetCurrent(target: ConversationTarget.Pm): Boolean {
|
||||
val dm = _dmConversations.value.find { it.convId == target.id } ?: _currentDm.value?.takeIf { it.convId == target.id }
|
||||
if (dm?.generation != target.generation || target.id in _dmDeleting.value) {
|
||||
LogManager.addLog("INFO", "PM", "Discarded stale text conv=${target.id} generation=${target.generation}"); return false
|
||||
LogManager.addLog("INFO", "PM", "Discarded stale operation conv=${target.id} generation=${target.generation}"); return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun sendDmText(target: ConversationTarget.Pm, text: String): Boolean {
|
||||
val r = repo ?: return false
|
||||
if (text.isBlank() || !dmTargetCurrent(target)) return false
|
||||
r.dmSend(target.id, text, target.generation)
|
||||
return true
|
||||
}
|
||||
@@ -1437,6 +1456,7 @@ class ChatViewModel : ViewModel() {
|
||||
private var recordingSubmission: Pair<Long, CompletableDeferred<Boolean>>? = null
|
||||
|
||||
fun startVoiceRecording(target: ConversationTarget) {
|
||||
if (target is ConversationTarget.Pm && !dmTargetCurrent(target)) return
|
||||
if (recordingStopping) { LogManager.addLog("WARN", "VM", "recording is still stopping"); return }
|
||||
if (_isRecording.value) { LogManager.addLog("WARN", "VM", "recording already active"); return }
|
||||
if (target is ConversationTarget.Channel && target.id in _submittingChannels.value) {
|
||||
@@ -1468,6 +1488,8 @@ class ChatViewModel : ViewModel() {
|
||||
|
||||
fun stopVoiceRecording() {
|
||||
if (!_isRecording.value || recordingStopping) return
|
||||
val target = recordingTarget
|
||||
if (target is ConversationTarget.Pm && !dmTargetCurrent(target)) { cancelVoiceRecording(target); return }
|
||||
recordingStopping = true
|
||||
_isRecording.value = false
|
||||
_isRecordingLocked.value = false
|
||||
@@ -1530,8 +1552,15 @@ class ChatViewModel : ViewModel() {
|
||||
fun sendAttachment(filePath: String, target: ConversationTarget, name: String = File(filePath).name,
|
||||
reply: ChannelReply? = replyFor(target)): Deferred<Boolean> = viewModelScope.async {
|
||||
if (target is ConversationTarget.Pm) {
|
||||
val accepted = NativeLib.dmSendFile(target.id, filePath, name, target.generation)
|
||||
if (!accepted) { LogManager.addLog("ERROR", "VM", "PM attachment rejected"); _messageSendFailed.value = true }
|
||||
val current = dmTargetCurrent(target)
|
||||
val accepted = current && NativeLib.dmSendFile(target.id, filePath, name, target.generation)
|
||||
if (!accepted) {
|
||||
withContext(kotlinx.coroutines.NonCancellable + Dispatchers.IO) {
|
||||
if (!File(filePath).delete() && File(filePath).exists())
|
||||
LogManager.addLog("ERROR", "PM", "Rejected attachment cache cleanup failed: $filePath")
|
||||
}
|
||||
if (current) { LogManager.addLog("ERROR", "VM", "PM attachment rejected"); _messageSendFailed.value = true }
|
||||
}
|
||||
return@async accepted
|
||||
}
|
||||
val chId = target.id
|
||||
@@ -1570,11 +1599,13 @@ class ChatViewModel : ViewModel() {
|
||||
} finally { mmr.release() }
|
||||
}
|
||||
val queue: (Long) -> Boolean = { request ->
|
||||
NativeLib.videoSend(target.nativeId, filePath, meta.first, meta.second, meta.third,
|
||||
if (target is ConversationTarget.Pm && !dmTargetCurrent(target)) {
|
||||
discardedPreparationJobs.add(job); false
|
||||
} else NativeLib.videoSend(target.nativeId, filePath, meta.first, meta.second, meta.third,
|
||||
reply?.message?.messageId.orEmpty(), request, (target as? ConversationTarget.Pm)?.generation ?: 0).also { accepted = it }
|
||||
}
|
||||
saved = if (target is ConversationTarget.Channel) submitChannel(target.id, reply, queue) else queue(0)
|
||||
if (!saved) _messageSendFailed.value = true
|
||||
if (!saved && job !in discardedPreparationJobs) _messageSendFailed.value = true
|
||||
if (saved && clearReply) finishReply(reply)
|
||||
} catch (e: kotlinx.coroutines.CancellationException) { throw e }
|
||||
catch (e: Exception) {
|
||||
|
||||
@@ -677,12 +677,15 @@ void utun_bridge_dm_request_conversations(void) {
|
||||
uasync_post(ua, dm_conversations_trampoline, req);
|
||||
}
|
||||
|
||||
void utun_bridge_dm_request_messages(const char* conv_id) {
|
||||
void utun_bridge_dm_request_messages(const char* conv_id, uint64_t generation) {
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
if (!conv_id || !conv_id[0] || !uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM history: invalid request/core stopped"); return; }
|
||||
if (!conv_id || !conv_id[0] || !generation || !uasync_is_running(ua)) {
|
||||
bridge_log(BLEV_WARN, "DM history: invalid request/core stopped"); return;
|
||||
}
|
||||
struct dm_messages_req* req = u_calloc(1, sizeof(*req));
|
||||
if (!req) { bridge_log(BLEV_ERROR, "DM history: OOM"); return; }
|
||||
req->inst = instance_lite_get_instance();
|
||||
req->generation = generation;
|
||||
snprintf(req->conv_id, sizeof(req->conv_id), "%s", conv_id);
|
||||
uasync_post(ua, dm_messages_trampoline, req);
|
||||
}
|
||||
@@ -2149,7 +2152,7 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmSend(
|
||||
if (c) (*env)->ReleaseStringUTFChars(env, convId, c);
|
||||
}
|
||||
|
||||
/* UI получает подтверждение commit через CHAT_EVT_DM_INVITE_RESULT, а не через return JNI. */
|
||||
/* UI получает подтверждение commit через CHAT_EVT_DM_DELETE_RESULT, а не через return JNI. */
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDmDelete(
|
||||
JNIEnv* env, jobject thiz, jstring conversation, jlong generation, jlong request) {
|
||||
(void)thiz;
|
||||
@@ -2230,10 +2233,10 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestCallRout
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestMessages(
|
||||
JNIEnv* env, jobject thiz, jstring convId) {
|
||||
JNIEnv* env, jobject thiz, jstring convId, jlong generation) {
|
||||
(void)thiz;
|
||||
const char* c = (*env)->GetStringUTFChars(env, convId, NULL);
|
||||
utun_bridge_dm_request_messages(c);
|
||||
utun_bridge_dm_request_messages(c, (uint64_t)generation);
|
||||
if (c) (*env)->ReleaseStringUTFChars(env, convId, c);
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,8 @@ void utun_bridge_collect_conn_metrics(uint64_t peer_node_id);
|
||||
void utun_bridge_dm_start(uint64_t node_id, const char* source_ch_id, const char* peer_name);
|
||||
void utun_bridge_dm_send(const char* conv_id, const char* text, uint64_t generation);
|
||||
void utun_bridge_dm_request_conversations(void);
|
||||
void utun_bridge_dm_request_messages(const char* conv_id);
|
||||
/* Снимок видимой истории указанного поколения; запоздалый запрос отбрасывается в uasync. */
|
||||
void utun_bridge_dm_request_messages(const char* conv_id, uint64_t generation);
|
||||
int utun_bridge_dm_send_file(const char* conv_id, const char* path, const char* name, uint64_t generation);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
Reference in new issue
Block a user