Keep malformed events and deleted database state out of Android UI

This commit is contained in:
evgeny committed 2026-10-29 13:49:57 +01:00
1 parent c5879042fb
commit 4a5a8cf057
2 files changed
+50 -53

No files matched your search

@@ -98,6 +98,7 @@ fun ProfileAvatarControls() {
var pickedAccount by rememberSaveable { mutableStateOf(0L) }
var pickedServer by rememberSaveable { mutableStateOf(0L) }
var cropUri by rememberSaveable { mutableStateOf<String?>(null) }
var pickerFailed by remember { mutableStateOf(false) }
val state = edits["profile"] ?: AvatarEditState.IDLE
LaunchedEffect(servers) {
if (servers.none { it.nodeId == selectedServer }) selectedServer = servers.firstOrNull()?.nodeId ?: 0L
@@ -128,12 +129,15 @@ fun ProfileAvatarControls() {
}
Row {
TextButton(enabled = selectedServer != 0L && state != AvatarEditState.WORKING, onClick = {
pickedAccount = repository.accountId; pickedServer = selectedServer; picker.launch("image/*")
pickedAccount = repository.accountId; pickedServer = selectedServer
try { pickerFailed = false; picker.launch("image/*") }
catch (e: Exception) { pickerFailed = true; LogManager.addLog("ERROR", "Avatar", "Profile image picker failed: $e") }
}) { Text(stringResource(R.string.avatar_upload)) }
TextButton(enabled = repository.accountId != 0L && state != AvatarEditState.WORKING, onClick = {
repository.editProfile(0, null, repository.accountId)
}) { Text(stringResource(R.string.avatar_remove)) }
}
if (pickerFailed) Text(stringResource(R.string.avatar_edit_failed), color = MaterialTheme.colorScheme.error)
when (state) {
AvatarEditState.WORKING -> LinearProgressIndicator(Modifier.fillMaxWidth())
AvatarEditState.ERROR -> Text(stringResource(R.string.avatar_edit_failed), color = MaterialTheme.colorScheme.error)
@@ -804,18 +804,8 @@ class ChatViewModel : ViewModel() {
} else refreshSelectedMemberDetail(channel, node)
}
27 -> { /* CONNECTING_NODES: [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
if (data == null || data.size < 3) return
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 2) return
val chId = String(data, 1, cl)
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val count = buf.getShort().toInt() and 0xFFFF
val set = HashSet<Long>()
for (i in 0 until count) {
if (buf.position() + 8 > data.size) break
set.add(buf.getLong())
}
_connecting.value = _connecting.value + (chId to set)
val snapshot = ChannelEventFrames.nodes(data)
_connecting.value = _connecting.value + (snapshot.channel to snapshot.ids.toSet())
}
28 -> { /* ADMIN_KEY_RECEIVED: [ch_id_len:1][ch_id:var][from_node_id:8] */
if (data == null || data.size < 10) return
@@ -828,50 +818,39 @@ class ChatViewModel : ViewModel() {
if (_currentChannel.value?.id == chId) refreshMembers(chId)
}
30 -> { /* INVITE_CANDIDATES: [ch_id_len:1][ch_id][auto_node_id:8][count:2][node_id:8]* */
if (data == null || data.size < 11) { LogManager.addLog("WARN", "VM", "INVITE_CANDIDATES too short"); return }
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 10) return
val chId = String(data, 1, cl)
val snapshot = ChannelEventFrames.nodes(data, candidates = true)
val chId = snapshot.channel
if (inviteChannelId != chId) return
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val autoId = buf.long
val count = buf.short.toInt() and 0xFFFF
val ids = mutableListOf<Long>()
for (i in 0 until count) {
if (buf.remaining() < 8) break
ids.add(buf.long)
}
LogManager.addLog("INFO", "VM", "INVITE_CANDIDATES ch=$chId auto=0x%016x count=%d".format(autoId, count))
val autoId = snapshot.automatic
val ids = snapshot.ids
LogManager.addLog("INFO", "VM", "INVITE_CANDIDATES ch=$chId auto=0x%016x count=%d".format(autoId, ids.size))
viewModelScope.launch {
val myId = NativeLib.getMyNodeId()
val nameOf = withContext(Dispatchers.IO) {
(repo?.getMembers(chId) ?: emptyList()).associate { it.nodeId to it.name }
try {
val myId = NativeLib.getMyNodeId()
val nameOf = withContext(Dispatchers.IO) {
(repo?.getMembers(chId) ?: emptyList()).associate { it.nodeId to it.name }
}
fun label(id: Long): String = nameOf[id]?.takeIf { it.isNotEmpty() } ?: "0x%016x".format(id)
val list = mutableListOf<InviteCandidate>()
list.add(InviteCandidate(autoId, label(autoId), isAuto = true, isSelf = autoId == myId))
for (id in ids) {
if (id == autoId) continue
list.add(InviteCandidate(id, label(id), isSelf = id == myId))
}
if (inviteChannelId != chId) return@launch
_inviteCandidates.value = list
requestInviteLink(chId, autoId)
} catch (e: kotlinx.coroutines.CancellationException) { throw e
} catch (e: Exception) {
LogManager.addLog("ERROR", "Invite", "Candidate names unavailable channel=$chId: $e")
}
fun label(id: Long): String = nameOf[id]?.takeIf { it.isNotEmpty() } ?: "0x%016x".format(id)
val list = mutableListOf<InviteCandidate>()
list.add(InviteCandidate(autoId, label(autoId), isAuto = true, isSelf = autoId == myId))
for (id in ids) {
if (id == autoId) continue
list.add(InviteCandidate(id, label(id), isSelf = id == myId))
}
if (inviteChannelId != chId) return@launch
_inviteCandidates.value = list
if (inviteChannelId == chId) requestInviteLink(chId, autoId)
}
}
29 -> { /* [ch_id_len:1][ch_id][request_id:8][link_len:2][link] */
if (data == null || data.size < 11) return
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 10) return
val chId = String(data, 1, cl)
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val requestId = buf.long
if (chId != inviteChannelId || requestId != inviteRequestId) return
val linkLen = buf.short.toInt() and 0xFFFF
val link = if (linkLen > 0 && buf.remaining() >= linkLen)
String(data, 1 + cl + 10, linkLen) else null
LogManager.addLog(if (link != null) "INFO" else "ERROR", "VM", "INVITE_LINK_READY ch=$chId len=$linkLen")
_inviteLink.value = link
val result = ChannelEventFrames.invite(data)
if (result.channel != inviteChannelId || result.request != inviteRequestId) return
LogManager.addLog(if (result.link != null) "INFO" else "ERROR", "VM", "INVITE_LINK_READY ch=${result.channel} bytes=${data?.size}")
_inviteLink.value = result.link
_inviteLinkPending.value = false
}
31 -> { /* CHANNEL_DELETED: [ch_id_len:1][ch_id:var] */
@@ -1921,12 +1900,23 @@ class ChatViewModel : ViewModel() {
}
}
/** Main, после подтверждённого join worker: убрать все ссылки на удаляемую БД и завершить её ожидающие отправки. */
fun clearState() {
cancelDmStart(); _dmStartFailed.value = false; _dmOpen.value = null
++channelRefreshRevision; ++messageRefreshRevision; ++jumpRevision
++editRevision; ++replyRevision
for (id in submissions.keys.toList()) submissionResult(id, false)
_editing.value = null; _replies.value = emptyMap(); _replyOriginals.value = emptyMap(); cameraReply = null
_historyAnchor.value = ""; _messageJump.value = null; pendingLatestJump = null; _originalMissing.value = false
feedbackCache.clear(); _reactionCatalog.value = emptyList()
memberOwnerJob?.cancel(); _members.value = emptyList(); _memberLinks.value = MemberLinks()
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null
_attachmentPreparations.value = emptyMap()
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null; _serverLink.value = null
inviteChannelId = null; _inviteCandidates.value = emptyList(); _inviteLink.value = null; _inviteLinkPending.value = false
_connecting.value = emptyMap(); dmPresenceRefreshJob?.cancel(); _dmCallAvailable.value = false
_currentDm.value = null; _dmConversations.value = emptyList(); _dmMessages.value = emptyList()
dmDeleteRequests.clear(); _dmDeleting.value = emptySet(); _dmDeleteFailed.value = false
_attachmentPreparations.value = emptyMap(); discardedPreparationJobs.clear()
repo?.close()
repo = null
dbReady = false
@@ -1937,6 +1927,9 @@ class ChatViewModel : ViewModel() {
_generatingKeys.value = false
_localSockets.value = emptyList()
_channelOwner.value = false
_playbackState.value = PlaybackState(); realPlaybackElapsedMs = 0
_radioActive.value = false; _radioTalking.value = null; _radioTalkState.value = RadioTalkState()
LogManager.addLog("INFO", "VM", "Database state cleared: conversations, drafts, members, history and pending submissions")
}
override fun onCleared() {