|
|
|
|
@ -25,6 +25,19 @@ import java.io.FileOutputStream
|
|
|
|
|
private val IMAGE_EXTENSIONS = setOf("jpg", "jpeg", "png", "gif", "bmp", "webp") |
|
|
|
|
|
|
|
|
|
class ChatViewModel : ViewModel() { |
|
|
|
|
data class PmInviteState(val peer: Long, val name: String, val source: String, val groups: List<Channel>, |
|
|
|
|
val groupId: String = "", val requestId: Long = 0, val pending: Boolean = false, val errorRes: Int = 0) |
|
|
|
|
private val _pmInvite = MutableStateFlow<PmInviteState?>(null) |
|
|
|
|
val pmInvite: StateFlow<PmInviteState?> = _pmInvite |
|
|
|
|
private var nextPmInviteId = 0L |
|
|
|
|
|
|
|
|
|
data class LinkJoinState(val link: String, val channelId: Long, val nodeId: Long, |
|
|
|
|
val pending: Boolean = true, val result: Int? = null) |
|
|
|
|
private val _linkJoin = MutableStateFlow<LinkJoinState?>(null) |
|
|
|
|
val linkJoin: StateFlow<LinkJoinState?> = _linkJoin |
|
|
|
|
private val _joinedChannel = MutableStateFlow<Channel?>(null) |
|
|
|
|
val joinedChannel: StateFlow<Channel?> = _joinedChannel |
|
|
|
|
|
|
|
|
|
data class AttachmentPreparation(val jobs: Set<String> = emptySet(), val failed: Boolean = false) |
|
|
|
|
private val _attachmentPreparations = MutableStateFlow<Map<String, AttachmentPreparation>>(emptyMap()) |
|
|
|
|
val attachmentPreparations: StateFlow<Map<String, AttachmentPreparation>> = _attachmentPreparations |
|
|
|
|
@ -317,6 +330,18 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
val nodeId = buf.getLong() |
|
|
|
|
val result = buf.getInt() |
|
|
|
|
val chId = buf.getLong() |
|
|
|
|
val joining = _linkJoin.value |
|
|
|
|
if (joining != null && joining.pending && joining.channelId == chId && joining.nodeId == nodeId) { |
|
|
|
|
_linkJoin.value = joining.copy(pending = false, result = result) |
|
|
|
|
if (result == 0) viewModelScope.launch { |
|
|
|
|
val channels = withContext(Dispatchers.IO) { repo?.getChannels().orEmpty() } |
|
|
|
|
_channels.value = channels |
|
|
|
|
val channel = channels.find { it.id.toULongOrNull()?.toLong() == chId } |
|
|
|
|
if (_linkJoin.value?.link == joining.link && channel != null) { |
|
|
|
|
_linkJoin.value = null; _joinedChannel.value = channel |
|
|
|
|
} else if (channel == null) LogManager.addLog("ERROR", "JOIN", "joined channel missing ch=$chId") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
LogManager.addLog( |
|
|
|
|
if (result == 0) "INFO" else "ERROR", |
|
|
|
|
"JOIN", "connect to 0x%016x ch=$chId result=$result".format(nodeId)) |
|
|
|
|
@ -326,6 +351,26 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
4 -> { if (repo != null) { LogManager.addLog("INFO", "VM", "CHANNEL_UPDATED — refreshChannels"); refreshChannels() } else { LogManager.addLog("WARN", "VM", "CHANNEL_UPDATED skipped — repo is NULL") } } |
|
|
|
|
12 -> LogManager.addLog("INFO", "VM", "service started") |
|
|
|
|
13 -> { _attachmentPreparations.value = emptyMap(); LogManager.addLog("INFO", "VM", "service stopped") } |
|
|
|
|
48 -> { /* [request_id:8][result:4][peer:8][conv:8] */ |
|
|
|
|
if (data == null || data.size != 28) { LogManager.addLog("ERROR", "INVITE", "invalid PM invite result"); return } |
|
|
|
|
val buf = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val request = buf.long; val result = buf.int; val peer = buf.long; val conv = buf.long.toULong().toString() |
|
|
|
|
val state = _pmInvite.value ?: return |
|
|
|
|
if (!state.pending || state.requestId != request || state.peer != peer) return |
|
|
|
|
LogManager.addLog(if (result == 0) "INFO" else "ERROR", "INVITE", "PM invite result request=$request peer=$peer result=$result") |
|
|
|
|
if (result == 0) { |
|
|
|
|
_pmInvite.value = null |
|
|
|
|
openDm(conv, peer, state.name); _dmOpen.value = conv; refreshDmConversations() |
|
|
|
|
} else { |
|
|
|
|
val error = when (result) { |
|
|
|
|
-1 -> R.string.invite_pm_start_failed |
|
|
|
|
-3 -> R.string.invite_pm_send_failed |
|
|
|
|
-5 -> R.string.invite_pm_busy |
|
|
|
|
else -> R.string.invite_build_failed |
|
|
|
|
} |
|
|
|
|
_pmInvite.value = state.copy(pending = false, errorRes = error) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
47 -> { |
|
|
|
|
if (data == null || data.size < 17 || data.size != 17 + (data[0].toInt() and 0xff)) { |
|
|
|
|
LogManager.addLog("WARN", "Call", "invalid PM route snapshot size=${data?.size}"); return |
|
|
|
|
@ -919,6 +964,59 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
r.dmSend(dm.convId, text) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun showPmInvite(peer: Long, name: String, source: String = "") { |
|
|
|
|
if (peer == 0L || peer == NativeLib.getMyNodeId()) { LogManager.addLog("ERROR", "INVITE", "invalid PM recipient"); return } |
|
|
|
|
if (_pmInvite.value != null) return |
|
|
|
|
val groups = _channels.value.filter { NativeLib.isChannelMember(it.id, NativeLib.getMyNodeId()) } |
|
|
|
|
_pmInvite.value = PmInviteState(peer, name, source, groups) |
|
|
|
|
LogManager.addLog("INFO", "INVITE", "PM group picker peer=$peer source=$source groups=${groups.size}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun selectPmInviteGroup(groupId: String) { |
|
|
|
|
_pmInvite.value?.let { if (!it.pending) _pmInvite.value = it.copy(groupId = groupId, errorRes = 0) } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun sendPmInvite() { |
|
|
|
|
val state = _pmInvite.value ?: return |
|
|
|
|
if (state.pending) return |
|
|
|
|
val channel = state.groupId.toULongOrNull()?.toLong() ?: return |
|
|
|
|
val request = ++nextPmInviteId |
|
|
|
|
_pmInvite.value = state.copy(requestId = request, pending = true, errorRes = 0) |
|
|
|
|
if (!NativeLib.dmInvite(request, state.peer, state.source, channel, state.name)) { |
|
|
|
|
_pmInvite.value = state.copy(errorRes = R.string.invite_core_unavailable) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun dismissPmInvite() { |
|
|
|
|
val state = _pmInvite.value ?: return |
|
|
|
|
_pmInvite.value = null |
|
|
|
|
if (state.pending) NativeLib.dmInvite(state.requestId, state.peer, state.source, 0, state.name, cancel = true) |
|
|
|
|
LogManager.addLog("INFO", "INVITE", "PM group picker closed request=${state.requestId} pending=${state.pending}") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun openInviteLink(link: String) { |
|
|
|
|
if (_linkJoin.value?.pending == true) return |
|
|
|
|
val invite = InviteLink.decode(link) |
|
|
|
|
if (invite == null) { |
|
|
|
|
LogManager.addLog("ERROR", "JOIN", "invalid message invite link") |
|
|
|
|
_linkJoin.value = LinkJoinState(link, 0, 0, pending = false, result = -1); return |
|
|
|
|
} |
|
|
|
|
val existing = _channels.value.find { it.id.toULongOrNull()?.toLong() == invite.channelId } |
|
|
|
|
if (existing != null && NativeLib.isChannelMember(existing.id, NativeLib.getMyNodeId())) { |
|
|
|
|
_linkJoin.value = null; _joinedChannel.value = existing; return |
|
|
|
|
} |
|
|
|
|
if (repo == null || !NativeLib.isReady()) { |
|
|
|
|
LogManager.addLog("ERROR", "JOIN", "core unavailable for message invite") |
|
|
|
|
_linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId, pending = false, result = -7); return |
|
|
|
|
} |
|
|
|
|
_linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId) |
|
|
|
|
val addresses = invite.serializeAddrs() |
|
|
|
|
joinChannel(invite.channelId, invite.nodeId, invite.joinKey, invite.pubkey, addresses, invite.addrs.size) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun dismissLinkJoin() { _linkJoin.value = null } |
|
|
|
|
fun consumeJoinedChannel() { _joinedChannel.value = null } |
|
|
|
|
|
|
|
|
|
fun sendDmFile(convId: String, path: String, name: String) { |
|
|
|
|
if (repo != null) NativeLib.dmSendFile(convId, path, name) |
|
|
|
|
} |
|
|
|
|
@ -1288,6 +1386,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun clearState() { |
|
|
|
|
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null |
|
|
|
|
_attachmentPreparations.value = emptyMap() |
|
|
|
|
repo?.close() |
|
|
|
|
repo = null |
|
|
|
|
|