diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 438e5ddd50..2778a8012c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -35,6 +35,7 @@ import kotlin.time.* class ChatModel(val controller: ChatController) { val onboardingStage = mutableStateOf(null) val currentUser = mutableStateOf(null) + val users = mutableStateListOf() val userCreated = mutableStateOf(null) val chatRunning = mutableStateOf(null) val chatDbChanged = mutableStateOf(false) @@ -42,6 +43,8 @@ class ChatModel(val controller: ChatController) { val chatDbStatus = mutableStateOf(null) val chatDbDeleted = mutableStateOf(false) val chats = mutableStateListOf() + // map of connections network statuses, key is agent connection id + val networkStatuses = mutableStateMapOf() // current chat val chatId = mutableStateOf(null) @@ -87,13 +90,6 @@ class ChatModel(val controller: ChatController) { val filesToDelete = mutableSetOf() val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get()) - fun updateUserProfile(profile: LocalProfile) { - val user = currentUser.value - if (user != null) { - currentUser.value = user.copy(profile = profile) - } - } - fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } @@ -120,17 +116,8 @@ class ChatModel(val controller: ChatController) { } fun updateChats(newChats: List) { - val mergedChats = arrayListOf() - for (newChat in newChats) { - val i = getChatIndex(newChat.chatInfo.id) - if (i >= 0) { - mergedChats.add(newChat.copy(serverInfo = chats[i].serverInfo)) - } else { - mergedChats.add(newChat) - } - } chats.clear() - chats.addAll(mergedChats) + chats.addAll(newChats) val cId = chatId.value // If chat is null, it was deleted in background after apiGetChats call @@ -139,14 +126,6 @@ class ChatModel(val controller: ChatController) { } } - fun updateNetworkStatus(id: ChatId, status: Chat.NetworkStatus) { - val i = getChatIndex(id) - if (i >= 0) { - val chat = chats[i] - chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status)) - } - } - fun replaceChat(id: String, chat: Chat) { val i = getChatIndex(id) if (i >= 0) { @@ -168,6 +147,7 @@ class ChatModel(val controller: ChatController) { chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId + increaseUnreadCounter(currentUser.value!!) chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId) } else @@ -256,6 +236,7 @@ class ChatModel(val controller: ChatController) { // clear preview val i = getChatIndex(cInfo.id) if (i >= 0) { + decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount) chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) } // clear current chat @@ -264,6 +245,18 @@ class ChatModel(val controller: ChatController) { } } + fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) { + val current = currentUser.value ?: return + val updated = current.copy( + profile = newProfile.toLocalProfile(current.profile.profileId), + fullPreferences = preferences ?: current.fullPreferences + ) + val indexInUsers = users.indexOfFirst { it.user.userId == current.userId } + if (indexInUsers != -1) { + users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount) + } + } + suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) withContext(Dispatchers.Main) { @@ -286,9 +279,11 @@ class ChatModel(val controller: ChatController) { val chat = chats[chatIdx] val lastId = chat.chatItems.lastOrNull()?.id if (lastId != null) { + val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 + decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount) chats[chatIdx] = chat.copy( chatStats = chat.chatStats.copy( - unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0, + unreadCount = unreadCount, // Can't use minUnreadItemId currently since chat items can have unread items between read items //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 ) @@ -324,13 +319,30 @@ class ChatModel(val controller: ChatController) { if (chatIndex == -1) return val chat = chats[chatIndex] + val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) + decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount) chats[chatIndex] = chat.copy( chatStats = chat.chatStats.copy( - unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0), + unreadCount = unreadCount, ) ) } + fun increaseUnreadCounter(user: User) { + changeUnreadCounter(user, 1) + } + + fun decreaseUnreadCounter(user: User, by: Int = 1) { + changeUnreadCounter(user, -by) + } + + private fun changeUnreadCounter(user: User, by: Int) { + val i = users.indexOfFirst { it.user.userId == user.userId } + if (i != -1) { + users[i] = UserInfo(user, users[i].unreadCount + by) + } + } + // func popChat(_ id: String) { // if let i = getChatIndex(id) { // popChat_(i) @@ -375,6 +387,13 @@ class ChatModel(val controller: ChatController) { false } } + + fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { + networkStatuses[contact.activeConn.agentConnId] = status + } + + fun contactNetworkStatus(contact: Contact): NetworkStatus = + networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown() } enum class ChatType(val type: String) { @@ -410,6 +429,19 @@ data class User( } } +@Serializable +data class UserInfo( + val user: User, + val unreadCount: Int +) { + companion object { + val sampleData = UserInfo( + user = User.sampleData, + unreadCount = 1 + ) + } +} + typealias ChatId = String interface NamedChat { @@ -441,37 +473,12 @@ data class Chat ( val chatInfo: ChatInfo, val chatItems: List, val chatStats: ChatStats = ChatStats(), - val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown()) ) { val id: String get() = chatInfo.id @Serializable data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false) - @Serializable - data class ServerInfo(val networkStatus: NetworkStatus) - - @Serializable - sealed class NetworkStatus { - val statusString: String get() = - when (this) { - is Connected -> generalGetString(R.string.server_connected) - is Error -> generalGetString(R.string.server_error) - else -> generalGetString(R.string.server_connecting) - } - val statusExplanation: String get() = - when (this) { - is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact) - is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error) - else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages) - } - - @Serializable @SerialName("unknown") class Unknown: NetworkStatus() - @Serializable @SerialName("connected") class Connected: NetworkStatus() - @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() - @Serializable @SerialName("error") class Error(val error: String): NetworkStatus() - } - companion object { val sampleData = Chat( chatInfo = ChatInfo.Direct.sampleData, @@ -605,6 +612,27 @@ sealed class ChatInfo: SomeChat, NamedChat { } } +@Serializable +sealed class NetworkStatus { + val statusString: String get() = + when (this) { + is Connected -> generalGetString(R.string.server_connected) + is Error -> generalGetString(R.string.server_error) + else -> generalGetString(R.string.server_connecting) + } + val statusExplanation: String get() = + when (this) { + is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact) + is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error) + else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages) + } + + @Serializable @SerialName("unknown") class Unknown: NetworkStatus() + @Serializable @SerialName("connected") class Connected: NetworkStatus() + @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() + @Serializable @SerialName("error") class Error(val error: String): NetworkStatus() +} + @Serializable data class Contact( val contactId: Long, @@ -675,6 +703,8 @@ data class Contact( @Serializable class ContactRef( val contactId: Long, + val agentConnId: String, + val connId: Long, var localDisplayName: String ) { val id: ChatId get() = "@$contactId" @@ -689,6 +719,7 @@ class ContactSubStatus( @Serializable data class Connection( val connId: Long, + val agentConnId: String, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, @@ -697,7 +728,7 @@ data class Connection( ) { val id: ChatId get() = ":$connId" companion object { - val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null) + val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index 91d3948a9b..7330db3651 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -77,8 +77,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference } } - fun notifyContactRequestReceived(cInfo: ChatInfo.ContactRequest) { + fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) { notifyMessageReceived( + user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = generalGetString(R.string.notification_new_contact_request), @@ -87,21 +88,22 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference ) } - fun notifyContactConnected(contact: Contact) { + fun notifyContactConnected(user: User, contact: Contact) { notifyMessageReceived( + user = user, chatId = contact.id, displayName = contact.displayName, msgText = generalGetString(R.string.notification_contact_connected) ) } - fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) { + fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) { if (!cInfo.ntfsEnabled) return - notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) } - fun notifyMessageReceived(chatId: String, displayName: String, msgText: String, image: String? = null, actions: List = emptyList()) { + fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List = emptyList()) { Log.d(TAG, "notifyMessageReceived $chatId") val now = Clock.System.now().toEpochMilliseconds() val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 311b14536e..a36afc3d65 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -266,18 +266,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a if (chatModel.chatRunning.value == true) return apiSetNetworkConfig(getNetCfg()) val justStarted = apiStartChat() + chatModel.users.clear() + chatModel.users.addAll(listUsers()) if (justStarted) { chatModel.currentUser.value = user chatModel.userCreated.value = true apiSetFilesFolder(getAppFilesDirectory(appContext)) apiSetIncognito(chatModel.incognito.value) - chatModel.userAddress.value = apiGetUserAddress() - val smpServers = getUserSMPServers() - chatModel.userSMPServers.value = smpServers?.first - chatModel.presetSMPServers.value = smpServers?.second - chatModel.chatItemTTL.value = getChatItemTTL() - val chats = apiGetChats() - chatModel.updateChats(chats) + getUserChatData() chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now()) chatModel.chatRunning.value = true @@ -294,6 +290,27 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + suspend fun changeActiveUser(toUserId: Long) { + try { + chatModel.currentUser.value = apiSetActiveUser(toUserId) + chatModel.users.clear() + chatModel.users.addAll(listUsers()) + getUserChatData() + } catch (e: Exception) { + Log.e(TAG, "Unable to set active user: ${e.stackTraceToString()}") + } + } + + suspend fun getUserChatData() { + chatModel.userAddress.value = apiGetUserAddress() + val smpServers = getUserSMPServers() + chatModel.userSMPServers.value = smpServers?.first + chatModel.presetSMPServers.value = smpServers?.second + chatModel.chatItemTTL.value = getChatItemTTL() + val chats = apiGetChats() + chatModel.updateChats(chats) + } + private fun startReceiver() { Log.d(TAG, "ChatController startReceiver") if (receiverStarted) return @@ -363,6 +380,27 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a throw Error("user not created ${r.responseType} ${r.details}") } + suspend fun listUsers(): List { + val r = sendCmd(CC.ListUsers()) + if (r is CR.UsersList) return r.users.sortedBy { it.user.chatViewName } + Log.d(TAG, "listUsers: ${r.responseType} ${r.details}") + throw Exception("failed to list users ${r.responseType} ${r.details}") + } + + suspend fun apiSetActiveUser(userId: Long): User { + val r = sendCmd(CC.ApiSetActiveUser(userId)) + if (r is CR.ActiveUser) return r.user + Log.d(TAG, "apiSetActiveUser: ${r.responseType} ${r.details}") + throw Exception("failed to set the user as active ${r.responseType} ${r.details}") + } + + suspend fun apiDeleteUser(userId: Long) { + val r = sendCmd(CC.ApiDeleteUser(userId)) + if (r is CR.CmdOk) return + Log.d(TAG, "apiDeleteUser: ${r.responseType} ${r.details}") + throw Exception("failed to delete the user ${r.responseType} ${r.details}") + } + suspend fun apiStartChat(): Boolean { val r = sendCmd(CC.StartChat(expire = true)) when (r) { @@ -721,13 +759,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } - suspend fun apiParseMarkdown(text: String): List? { - val r = sendCmd(CC.ApiParseMarkdown(text)) - if (r is CR.ParsedMarkdown) return r.formattedText - Log.e(TAG, "apiParseMarkdown bad response: ${r.responseType} ${r.details}") - return null - } - suspend fun apiSetContactPrefs(contactId: Long, prefs: ChatPreferences): Contact? { val r = sendCmd(CC.ApiSetContactPrefs(contactId, prefs)) if (r is CR.ContactPrefsUpdated) return r.toContact @@ -1067,6 +1098,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + suspend fun apiParseMarkdown(text: String): List? { + val r = sendCmd(CC.ApiParseMarkdown(text)) + if (r is CR.ParsedMarkdown) return r.formattedText + Log.e(TAG, "apiParseMarkdown bad response: ${r.responseType} ${r.details}") + return null + } + private fun networkErrorAlert(r: CR): Boolean { return when { r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent @@ -1102,62 +1140,81 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a chatModel.terminalItems.add(TerminalItem.resp(r)) when (r) { is CR.NewContactConnection -> { - chatModel.updateContactConnection(r.connection) + if (active(r.user)) { + chatModel.updateContactConnection(r.connection) + } } is CR.ContactConnectionDeleted -> { - chatModel.removeChat(r.connection.id) + if (active(r.user)) { + chatModel.removeChat(r.connection.id) + } } is CR.ContactConnected -> { - if (r.contact.directOrUsed) { + if (active(r.user) && r.contact.directOrUsed) { chatModel.updateContact(r.contact) chatModel.dismissConnReqView(r.contact.activeConn.id) chatModel.removeChat(r.contact.activeConn.id) - chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected()) - ntfManager.notifyContactConnected(r.contact) + ntfManager.notifyContactConnected(r.user, r.contact) } + chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) } is CR.ContactConnecting -> { - if (r.contact.directOrUsed) { + if (active(r.user) && r.contact.directOrUsed) { chatModel.updateContact(r.contact) chatModel.dismissConnReqView(r.contact.activeConn.id) chatModel.removeChat(r.contact.activeConn.id) } } is CR.ReceivedContactRequest -> { + if (!active(r.user)) return + val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf())) - ntfManager.notifyContactRequestReceived(cInfo) + ntfManager.notifyContactRequestReceived(r.user, cInfo) } is CR.ContactUpdated -> { - val cInfo = ChatInfo.Direct(r.toContact) - if (chatModel.hasChat(r.toContact.id)) { + if (active(r.user) && chatModel.hasChat(r.toContact.id)) { + val cInfo = ChatInfo.Direct(r.toContact) chatModel.updateChatInfo(cInfo) } } is CR.ContactsMerged -> { - if (chatModel.hasChat(r.mergedContact.id)) { + if (active(r.user) && chatModel.hasChat(r.mergedContact.id)) { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } chatModel.removeChat(r.mergedContact.id) } } - is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Connected()) - is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Disconnected()) - is CR.ContactSubError -> processContactSubError(r.contact, r.chatError) + is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) + is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) + is CR.ContactSubError -> { + if (active(r.user)) { + chatModel.updateContact(r.contact) + } + processContactSubError(r.contact, r.chatError) + } is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { + if (active(r.user)) { + chatModel.updateContact(sub.contact) + } val err = sub.contactError if (err == null) { - chatModel.updateContact(sub.contact) - chatModel.updateNetworkStatus(sub.contact.id, Chat.NetworkStatus.Connected()) + chatModel.setContactNetworkStatus(sub.contact, NetworkStatus.Connected()) } else { processContactSubError(sub.contact, sub.contactError) } } } is CR.NewChatItem -> { + if (!active(r.user)) { + if (r.chatItem.chatItem.isRcvNew && r.chatItem.chatInfo.ntfsEnabled) { + chatModel.increaseUnreadCounter(r.user) + } + return + } val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) @@ -1171,10 +1228,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } if (cItem.showNotification && (!SimplexApp.context.isAppOnForeground || chatModel.chatId.value != cInfo.id)) { - ntfManager.notifyMessageReceived(cInfo, cItem) + ntfManager.notifyMessageReceived(r.user, cInfo, cItem) } } is CR.ChatItemStatusUpdated -> { + if (!active(r.user)) return + val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem var res = false @@ -1182,12 +1241,21 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a res = chatModel.upsertChatItem(cInfo, cItem) } if (res) { - ntfManager.notifyMessageReceived(cInfo, cItem) + ntfManager.notifyMessageReceived(r.user, cInfo, cItem) } } is CR.ChatItemUpdated -> - chatItemSimpleUpdate(r.chatItem) + if (active(r.user)) { + chatItemSimpleUpdate(r.chatItem) + } is CR.ChatItemDeleted -> { + if (!active(r.user)) { + if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) { + chatModel.decreaseUnreadCounter(r.user) + } + return + } + val cInfo = r.deletedChatItem.chatInfo val cItem = r.deletedChatItem.chatItem AudioPlayer.stop(cItem) @@ -1195,6 +1263,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { ntfManager.cancelNotificationsForChat(cInfo.id) ntfManager.notifyMessageReceived( + r.user, cInfo.id, cInfo.displayName, generalGetString(if (r.toChatItem != null) R.string.marked_deleted_description else R.string.deleted_description) @@ -1207,10 +1276,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } is CR.ReceivedGroupInvitation -> { - chatModel.updateGroup(r.groupInfo) // update so that repeat group invitations are not duplicated - // TODO NtfManager.shared.notifyGroupInvitation + if (active(r.user)) { + chatModel.updateGroup(r.groupInfo) // update so that repeat group invitations are not duplicated + // TODO NtfManager.shared.notifyGroupInvitation + } } is CR.UserAcceptedGroupSent -> { + if (!active(r.user)) return + chatModel.updateGroup(r.groupInfo) if (r.hostContact != null) { chatModel.dismissConnReqView(r.hostContact.activeConn.id) @@ -1218,34 +1291,64 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } is CR.JoinedGroupMemberConnecting -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.DeletedMemberUser -> // TODO update user member - chatModel.updateGroup(r.groupInfo) + if (active(r.user)) { + chatModel.updateGroup(r.groupInfo) + } is CR.DeletedMember -> - chatModel.upsertGroupMember(r.groupInfo, r.deletedMember) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.deletedMember) + } is CR.LeftMember -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.MemberRole -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.MemberRoleUser -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.GroupDeleted -> // TODO update user member - chatModel.updateGroup(r.groupInfo) + if (active(r.user)) { + chatModel.updateGroup(r.groupInfo) + } is CR.UserJoinedGroup -> - chatModel.updateGroup(r.groupInfo) + if (active(r.user)) { + chatModel.updateGroup(r.groupInfo) + } is CR.JoinedGroupMember -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.ConnectedToGroupMember -> - chatModel.upsertGroupMember(r.groupInfo, r.member) + if (active(r.user)) { + chatModel.upsertGroupMember(r.groupInfo, r.member) + } is CR.GroupUpdated -> - chatModel.updateGroup(r.toGroup) + if (active(r.user)) { + chatModel.updateGroup(r.toGroup) + } is CR.RcvFileStart -> - chatItemSimpleUpdate(r.chatItem) + if (active(r.user)) { + chatItemSimpleUpdate(r.chatItem) + } is CR.RcvFileComplete -> - chatItemSimpleUpdate(r.chatItem) + if (active(r.user)) { + chatItemSimpleUpdate(r.chatItem) + } is CR.SndFileStart -> - chatItemSimpleUpdate(r.chatItem) + if (active(r.user)) { + chatItemSimpleUpdate(r.chatItem) + } is CR.SndFileComplete -> { + if (!active(r.user)) return + chatItemSimpleUpdate(r.chatItem) val cItem = r.chatItem.chatItem val mc = cItem.content.msgContent @@ -1307,6 +1410,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } + private fun active(user: User): Boolean = user.userId == chatModel.currentUser.value?.userId + private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) { val call = chatModel.activeCall.value if (call != null && call.contact.apiId == contact.apiId) { @@ -1335,18 +1440,17 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem if (chatModel.upsertChatItem(cInfo, cItem)) { - ntfManager.notifyMessageReceived(cInfo, cItem) + ntfManager.notifyMessageReceived(chatModel.currentUser.value!!, cInfo, cItem) } } - fun updateContactsStatus(contactRefs: List, status: Chat.NetworkStatus) { + private fun updateContactsStatus(contactRefs: List, status: NetworkStatus) { for (c in contactRefs) { - chatModel.updateNetworkStatus(c.id, status) + chatModel.networkStatuses[c.agentConnId] = status } } - fun processContactSubError(contact: Contact, chatError: ChatError) { - chatModel.updateContact(contact) + private fun processContactSubError(contact: Contact, chatError: ChatError) { val e = chatError val err: String = if (e is ChatError.ChatErrorAgent) { @@ -1358,7 +1462,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } } else e.string - chatModel.updateNetworkStatus(contact.id, Chat.NetworkStatus.Error(err)) + chatModel.setContactNetworkStatus(contact, NetworkStatus.Error(err)) } fun showBackgroundServiceNoticeIfNeeded() { @@ -1621,6 +1725,9 @@ sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() class CreateActiveUser(val profile: Profile): CC() + class ListUsers: CC() + class ApiSetActiveUser(val userId: Long): CC() + class ApiDeleteUser(val userId: Long): CC() class StartChat(val expire: Boolean): CC() class ApiStopChat: CC() class SetFilesFolder(val filesFolder: String): CC() @@ -1693,6 +1800,9 @@ sealed class CC { is Console -> cmd is ShowActiveUser -> "/u" is CreateActiveUser -> "/create user ${profile.displayName} ${profile.fullName}" + is ListUsers -> "/users" + is ApiSetActiveUser -> "/_user $userId" + is ApiDeleteUser -> "/_delete user $userId" is StartChat -> "/_start subscribe=on expire=${onOff(expire)}" is ApiStopChat -> "/_stop" is SetFilesFolder -> "/_files_folder $filesFolder" @@ -1701,7 +1811,7 @@ sealed class CC { is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}" - is ApiGetChats -> "/_get $userId chats pcc=on" + is ApiGetChats -> "/_get chats $userId pcc=on" is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") is ApiSendMessage -> "/_send ${chatRef(type, id)} live=${onOff(live)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}" is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" @@ -1766,6 +1876,9 @@ sealed class CC { is Console -> "console command" is ShowActiveUser -> "showActiveUser" is CreateActiveUser -> "createActiveUser" + is ListUsers -> "listUsers" + is ApiSetActiveUser -> "apiSetActiveUser" + is ApiDeleteUser -> "apiDeleteUser" is StartChat -> "startChat" is ApiStopChat -> "apiStopChat" is SetFilesFolder -> "setFilesFolder" @@ -2689,17 +2802,19 @@ class APIResponse(val resp: CR, val corr: String? = null) { val type = resp["type"]?.jsonPrimitive?.content ?: "invalid" try { if (type == "apiChats") { + val user: User = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chats: List = resp["chats"]!!.jsonArray.map { parseChatData(it) } return APIResponse( - resp = CR.ApiChats(chats), + resp = CR.ApiChats(user, chats), corr = data["corr"]?.toString() ) } else if (type == "apiChat") { + val user: User = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chat = parseChatData(resp["chat"]!!) return APIResponse( - resp = CR.ApiChat(chat), + resp = CR.ApiChat(user, chat), corr = data["corr"]?.toString() ) } @@ -2735,108 +2850,110 @@ private fun decodeObject(deserializer: DeserializationStrategy, obj: Json @Serializable sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() + @Serializable @SerialName("usersList") class UsersList(val users: List): CR() @Serializable @SerialName("chatStarted") class ChatStarted: CR() @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("chatStopped") class ChatStopped: CR() - @Serializable @SerialName("apiChats") class ApiChats(val chats: List): CR() - @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() - @Serializable @SerialName("userSMPServers") class UserSMPServers(val smpServers: List, val presetSMPServers: List): CR() - @Serializable @SerialName("smpTestResult") class SmpTestResult(val smpTestFailure: SMPTestFailure? = null): CR() - @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val chatItemTTL: Long? = null): CR() + @Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List): CR() + @Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR() + @Serializable @SerialName("userSMPServers") class UserSMPServers(val user: User, val smpServers: List, val presetSMPServers: List): CR() + @Serializable @SerialName("smpTestResult") class SmpTestResult(val user: User, val smpTestFailure: SMPTestFailure? = null): CR() + @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() - @Serializable @SerialName("contactInfo") class ContactInfo(val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() - @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats?): CR() - @Serializable @SerialName("contactCode") class ContactCode(val contact: Contact, val connectionCode: String): CR() - @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() - @Serializable @SerialName("connectionVerified") class ConnectionVerified(val verified: Boolean, val expectedCode: String): CR() - @Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR() - @Serializable @SerialName("sentConfirmation") class SentConfirmation: CR() - @Serializable @SerialName("sentInvitation") class SentInvitation: CR() - @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val contact: Contact): CR() - @Serializable @SerialName("contactDeleted") class ContactDeleted(val contact: Contact): CR() - @Serializable @SerialName("chatCleared") class ChatCleared(val chatInfo: ChatInfo): CR() - @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR() - @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() - @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR() - @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR() - @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val fromContact: Contact, val toContact: Contact): CR() - @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() - @Serializable @SerialName("userContactLink") class UserContactLink(val contactLink: UserContactLinkRec): CR() - @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val contactLink: UserContactLinkRec): CR() - @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR() - @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted: CR() - @Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact, val userCustomProfile: Profile? = null): CR() - @Serializable @SerialName("contactConnecting") class ContactConnecting(val contact: Contact): CR() - @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val contactRequest: UserContactRequest): CR() - @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val contact: Contact): CR() - @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected: CR() - @Serializable @SerialName("contactUpdated") class ContactUpdated(val toContact: Contact): CR() + @Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() + @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats?): CR() + @Serializable @SerialName("contactCode") class ContactCode(val user: User, val contact: Contact, val connectionCode: String): CR() + @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() + @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: User, val verified: Boolean, val expectedCode: String): CR() + @Serializable @SerialName("invitation") class Invitation(val user: User, val connReqInvitation: String): CR() + @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: User): CR() + @Serializable @SerialName("sentInvitation") class SentInvitation(val user: User): CR() + @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: User, val contact: Contact): CR() + @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: User, val contact: Contact): CR() + @Serializable @SerialName("chatCleared") class ChatCleared(val user: User, val chatInfo: ChatInfo): CR() + @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR() + @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile): CR() + @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val user: User, val toContact: Contact): CR() + @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val user: User, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: User, val fromContact: Contact, val toContact: Contact): CR() + @Serializable @SerialName("userContactLink") class UserContactLink(val user: User, val contactLink: UserContactLinkRec): CR() + @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val user: User, val contactLink: UserContactLinkRec): CR() + @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connReqContact: String): CR() + @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() + @Serializable @SerialName("contactConnected") class ContactConnected(val user: User, val contact: Contact, val userCustomProfile: Profile? = null): CR() + @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: User, val contact: Contact): CR() + @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: User, val contactRequest: UserContactRequest): CR() + @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: User, val contact: Contact): CR() + @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: User): CR() + @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: User, val toContact: Contact): CR() @Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List): CR() @Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List): CR() - @Serializable @SerialName("contactSubError") class ContactSubError(val contact: Contact, val chatError: ChatError): CR() - @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val contactSubscriptions: List): CR() - @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val group: GroupInfo): CR() - @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val memberSubErrors: List): CR() - @Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR() + @Serializable @SerialName("contactSubError") class ContactSubError(val user: User, val contact: Contact, val chatError: ChatError): CR() + @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: User, val contactSubscriptions: List): CR() + @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: User, val group: GroupInfo): CR() + @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: User, val memberSubErrors: List): CR() + @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: User, val group: GroupInfo): CR() @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() - @Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() - @Serializable @SerialName("contactsList") class ContactsList(val contacts: List): CR() + @Serializable @SerialName("newChatItem") class NewChatItem(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: User, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() + @Serializable @SerialName("contactsList") class ContactsList(val user: User, val contacts: List): CR() // group events - @Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR() - @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() - @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() - @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR() - @Serializable @SerialName("groupMembers") class GroupMembers(val group: Group): CR() - @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val groupInfo: GroupInfo): CR() - @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() - @Serializable @SerialName("memberRole") class MemberRole(val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("deletedMember") class DeletedMember(val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() - @Serializable @SerialName("leftMember") class LeftMember(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("groupDeleted") class GroupDeleted(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("contactsMerged") class ContactsMerged(val intoContact: Contact, val mergedContact: Contact): CR() - @Serializable @SerialName("groupInvitation") class GroupInvitation(val groupInfo: GroupInfo): CR() // unused - @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val groupInfo: GroupInfo): CR() - @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() // unused - @Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val groupInfo: GroupInfo, val connReqContact: String): CR() - @Serializable @SerialName("groupLink") class GroupLink(val groupInfo: GroupInfo, val connReqContact: String): CR() - @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupCreated") class GroupCreated(val user: User, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: User, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() + @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: User, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() + @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: User, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupMembers") class GroupMembers(val user: User, val group: Group): CR() + @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: User, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: User, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: User, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() + @Serializable @SerialName("memberRole") class MemberRole(val user: User, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("deletedMember") class DeletedMember(val user: User, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() + @Serializable @SerialName("leftMember") class LeftMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("groupDeleted") class GroupDeleted(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("contactsMerged") class ContactsMerged(val user: User, val intoContact: Contact, val mergedContact: Contact): CR() + @Serializable @SerialName("groupInvitation") class GroupInvitation(val user: User, val groupInfo: GroupInfo): CR() // unused + @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: User, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused + @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR() + @Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR() + @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR() // receiving file events - @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileStart") class RcvFileStart(val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): CR() // sending file events - @Serializable @SerialName("sndFileStart") class SndFileStart(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileComplete") class SndFileComplete(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR() + @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List): CR() @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() - @Serializable @SerialName("callOffer") class CallOffer(val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() - @Serializable @SerialName("callAnswer") class CallAnswer(val contact: Contact, val answer: WebRTCSession): CR() - @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() - @Serializable @SerialName("callEnded") class CallEnded(val contact: Contact): CR() - @Serializable @SerialName("newContactConnection") class NewContactConnection(val connection: PendingContactConnection): CR() - @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val connection: PendingContactConnection): CR() + @Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() + @Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR() + @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: User, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() + @Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR() + @Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR() + @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR() @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): CR() - @Serializable @SerialName("cmdOk") class CmdOk: CR() - @Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR() - @Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR() + @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() + @Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR() + @Serializable @SerialName("chatCmdError") class ChatCmdError(val user: User?, val chatError: ChatError): CR() + @Serializable @SerialName("chatError") class ChatRespError(val user: User?, val chatError: ChatError): CR() @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() val responseType: String get() = when(this) { is ActiveUser -> "activeUser" + is UsersList -> "usersList" is ChatStarted -> "chatStarted" is ChatRunning -> "chatRunning" is ChatStopped -> "chatStopped" @@ -2862,7 +2979,6 @@ sealed class CR { is ContactAliasUpdated -> "contactAliasUpdated" is ConnectionAliasUpdated -> "connectionAliasUpdated" is ContactPrefsUpdated -> "contactPrefsUpdated" - is ParsedMarkdown -> "apiParsedMarkdown" is UserContactLink -> "userContactLink" is UserContactLinkUpdated -> "userContactLinkUpdated" is UserContactLinkCreated -> "userContactLinkCreated" @@ -2928,6 +3044,7 @@ sealed class CR { is NewContactConnection -> "newContactConnection" is ContactConnectionDeleted -> "contactConnectionDeleted" is VersionInfo -> "versionInfo" + is ParsedMarkdown -> "apiParsedMarkdown" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" @@ -2936,106 +3053,109 @@ sealed class CR { } val details: String get() = when(this) { - is ActiveUser -> json.encodeToString(user) + is ActiveUser -> withUser(user, json.encodeToString(user)) + is UsersList -> json.encodeToString(users) is ChatStarted -> noDetails() is ChatRunning -> noDetails() is ChatStopped -> noDetails() - is ApiChats -> json.encodeToString(chats) - is ApiChat -> json.encodeToString(chat) - is UserSMPServers -> "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}" - is SmpTestResult -> json.encodeToString(smpTestFailure) - is ChatItemTTL -> json.encodeToString(chatItemTTL) + is ApiChats -> withUser(user, json.encodeToString(chats)) + is ApiChat -> withUser(user, json.encodeToString(chat)) + is UserSMPServers -> withUser(user, "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}") + is SmpTestResult -> withUser(user, json.encodeToString(smpTestFailure)) + is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL)) is NetworkConfig -> json.encodeToString(networkConfig) - is ContactInfo -> "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}" - is GroupMemberInfo -> "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}" - is ContactCode -> "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode" - is GroupMemberCode -> "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode" - is ConnectionVerified -> "verified: $verified\nconnectionCode: $expectedCode" - is Invitation -> connReqInvitation - is SentConfirmation -> noDetails() - is SentInvitation -> noDetails() - is ContactAlreadyExists -> json.encodeToString(contact) - is ContactDeleted -> json.encodeToString(contact) - is ChatCleared -> json.encodeToString(chatInfo) - is UserProfileNoChange -> noDetails() - is UserProfileUpdated -> json.encodeToString(toProfile) - is ContactAliasUpdated -> json.encodeToString(toContact) - is ConnectionAliasUpdated -> json.encodeToString(toConnection) - is ContactPrefsUpdated -> "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}" + is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") + is GroupMemberInfo -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") + is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode") + is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode") + is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") + is Invitation -> withUser(user, connReqInvitation) + is SentConfirmation -> withUser(user, noDetails()) + is SentInvitation -> withUser(user, noDetails()) + is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) + is ContactDeleted -> withUser(user, json.encodeToString(contact)) + is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) + is UserProfileNoChange -> withUser(user, noDetails()) + is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile)) + is ContactAliasUpdated -> withUser(user, json.encodeToString(toContact)) + is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection)) + is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") is ParsedMarkdown -> json.encodeToString(formattedText) - is UserContactLink -> contactLink.responseDetails - is UserContactLinkUpdated -> contactLink.responseDetails - is UserContactLinkCreated -> connReqContact - is UserContactLinkDeleted -> noDetails() - is ContactConnected -> json.encodeToString(contact) - is ContactConnecting -> json.encodeToString(contact) - is ReceivedContactRequest -> json.encodeToString(contactRequest) - is AcceptingContactRequest -> json.encodeToString(contact) - is ContactRequestRejected -> noDetails() - is ContactUpdated -> json.encodeToString(toContact) + is UserContactLink -> withUser(user, contactLink.responseDetails) + is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) + is UserContactLinkCreated -> withUser(user, connReqContact) + is UserContactLinkDeleted -> withUser(user, noDetails()) + is ContactConnected -> withUser(user, json.encodeToString(contact)) + is ContactConnecting -> withUser(user, json.encodeToString(contact)) + is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest)) + is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) + is ContactRequestRejected -> withUser(user, noDetails()) + is ContactUpdated -> withUser(user, json.encodeToString(toContact)) is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" - is ContactSubError -> "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}" - is ContactSubSummary -> json.encodeToString(contactSubscriptions) - is GroupSubscribed -> json.encodeToString(group) - is MemberSubErrors -> json.encodeToString(memberSubErrors) - is GroupEmpty -> json.encodeToString(group) + is ContactSubError -> withUser(user, "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}") + is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions)) + is GroupSubscribed -> withUser(user, json.encodeToString(group)) + is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors)) + is GroupEmpty -> withUser(user, json.encodeToString(group)) is UserContactLinkSubscribed -> noDetails() - is NewChatItem -> json.encodeToString(chatItem) - is ChatItemStatusUpdated -> json.encodeToString(chatItem) - is ChatItemUpdated -> json.encodeToString(chatItem) - is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser" - is ContactsList -> json.encodeToString(contacts) - is GroupCreated -> json.encodeToString(groupInfo) - is SentGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact\nmember: $member" + is NewChatItem -> withUser(user, json.encodeToString(chatItem)) + is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem)) + is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) + is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser") + is ContactsList -> withUser(user, json.encodeToString(contacts)) + is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) + is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) - is UserDeletedMember -> "groupInfo: $groupInfo\nmember: $member" - is LeftMemberUser -> json.encodeToString(groupInfo) - is GroupMembers -> json.encodeToString(group) - is ReceivedGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole" - is GroupDeletedUser -> json.encodeToString(groupInfo) - is JoinedGroupMemberConnecting -> "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member" - is MemberRole -> "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole" - is MemberRoleUser -> "groupInfo: $groupInfo\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole" - is DeletedMemberUser -> "groupInfo: $groupInfo\nmember: $member" - is DeletedMember -> "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember" - is LeftMember -> "groupInfo: $groupInfo\nmember: $member" - is GroupDeleted -> "groupInfo: $groupInfo\nmember: $member" - is ContactsMerged -> "intoContact: $intoContact\nmergedContact: $mergedContact" - is GroupInvitation -> json.encodeToString(groupInfo) - is UserJoinedGroup -> json.encodeToString(groupInfo) - is JoinedGroupMember -> "groupInfo: $groupInfo\nmember: $member" - is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member" - is GroupRemoved -> json.encodeToString(groupInfo) - is GroupUpdated -> json.encodeToString(toGroup) - is GroupLinkCreated -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact" - is GroupLink -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact" - is GroupLinkDeleted -> json.encodeToString(groupInfo) - is RcvFileAcceptedSndCancelled -> noDetails() - is RcvFileAccepted -> json.encodeToString(chatItem) - is RcvFileStart -> json.encodeToString(chatItem) - is RcvFileComplete -> json.encodeToString(chatItem) + is UserDeletedMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo)) + is GroupMembers -> withUser(user, json.encodeToString(group)) + is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole") + is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo)) + is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") + is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") + is MemberRoleUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") + is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") + is LeftMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is GroupDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is ContactsMerged -> withUser(user, "intoContact: $intoContact\nmergedContact: $mergedContact") + is GroupInvitation -> withUser(user, json.encodeToString(groupInfo)) + is UserJoinedGroup -> withUser(user, json.encodeToString(groupInfo)) + is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) + is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) + is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") + is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact") + is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) + is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) + is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) + is RcvFileStart -> withUser(user, json.encodeToString(chatItem)) + is RcvFileComplete -> withUser(user, json.encodeToString(chatItem)) is SndFileCancelled -> json.encodeToString(chatItem) - is SndFileComplete -> json.encodeToString(chatItem) - is SndFileRcvCancelled -> json.encodeToString(chatItem) - is SndFileStart -> json.encodeToString(chatItem) - is SndGroupFileCancelled -> json.encodeToString(chatItem) + is SndFileComplete -> withUser(user, json.encodeToString(chatItem)) + is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem)) + is SndFileStart -> withUser(user, json.encodeToString(chatItem)) + is SndGroupFileCancelled -> withUser(user, json.encodeToString(chatItem)) is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" - is CallOffer -> "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}" - is CallAnswer -> "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}" - is CallExtraInfo -> "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}" - is CallEnded -> "contact: ${contact.id}" - is NewContactConnection -> json.encodeToString(connection) - is ContactConnectionDeleted -> json.encodeToString(connection) + is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") + is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}") + is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}") + is CallEnded -> withUser(user, "contact: ${contact.id}") + is NewContactConnection -> withUser(user, json.encodeToString(connection)) + is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) is VersionInfo -> json.encodeToString(versionInfo) - is CmdOk -> noDetails() - is ChatCmdError -> chatError.string - is ChatRespError -> chatError.string + is CmdOk -> withUser(user, noDetails()) + is ChatCmdError -> withUser(user, chatError.string) + is ChatRespError -> withUser(user, chatError.string) is Response -> json is Invalid -> str } fun noDetails(): String ="${responseType}: " + generalGetString(R.string.no_details) + + private fun withUser(u: User?, s: String): String = if (u != null) "userId: ${u.userId}\n$s" else s } abstract class TerminalItem { @@ -3111,11 +3231,13 @@ sealed class ChatError { sealed class ChatErrorType { val string: String get() = when (this) { is NoActiveUser -> "noActiveUser" + is DifferentActiveUser -> "differentActiveUser" is InvalidConnReq -> "invalidConnReq" is FileAlreadyReceiving -> "fileAlreadyReceiving" is СommandError -> "commandError $message" } @Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType() + @Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType() @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() @Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType() @Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index b827e92b89..18ec98fc7b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -102,7 +102,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt index bfb3ef089f..dc5286fd6e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -54,10 +54,14 @@ fun ChatInfoView( val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null) { + val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) { + mutableStateOf(chatModel.contactNetworkStatus(contact)) + } ChatInfoLayout( chat, contact, connStats, + contactNetworkStatus.value, customUserProfile, localAlias, connectionCode, @@ -149,6 +153,7 @@ fun ChatInfoLayout( chat: Chat, contact: Contact, connStats: ConnectionStats?, + contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, connectionCode: String?, @@ -200,9 +205,9 @@ fun ChatInfoLayout( SectionItemView({ AlertManager.shared.showAlertMsg( generalGetString(R.string.network_status), - chat.serverInfo.networkStatus.statusExplanation + contactNetworkStatus.statusExplanation )}) { - NetworkStatusRow(chat.serverInfo.networkStatus) + NetworkStatusRow(contactNetworkStatus) } val rcvServers = connStats.rcvServers if (rcvServers != null && rcvServers.isNotEmpty()) { @@ -314,7 +319,7 @@ fun LocalAliasEditor( } @Composable -private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) { +private fun NetworkStatusRow(networkStatus: NetworkStatus) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -346,14 +351,14 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) { } @Composable -private fun ServerImage(networkStatus: Chat.NetworkStatus) { +private fun ServerImage(networkStatus: NetworkStatus) { Box(Modifier.size(18.dp)) { when (networkStatus) { - is Chat.NetworkStatus.Connected -> + is NetworkStatus.Connected -> Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant) - is Chat.NetworkStatus.Disconnected -> + is NetworkStatus.Disconnected -> Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight) - is Chat.NetworkStatus.Error -> + is NetworkStatus.Error -> Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight) else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight) } @@ -446,14 +451,14 @@ fun PreviewChatInfoLayout() { ChatInfoLayout( chat = Chat( chatInfo = ChatInfo.Direct.sampleData, - chatItems = arrayListOf(), - serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) + chatItems = arrayListOf() ), Contact.sampleData, localAlias = "", connectionCode = "123", developerTools = false, connStats = null, + contactNetworkStatus = NetworkStatus.Connected(), onLocalAliasChanged = {}, customUserProfile = null, openPreferences = {}, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt index 874c0db684..24b2b9ab3f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt @@ -427,8 +427,7 @@ fun PreviewGroupChatInfoLayout() { GroupChatInfoLayout( chat = Chat( chatInfo = ChatInfo.Direct.sampleData, - chatItems = arrayListOf(), - serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) + chatItems = arrayListOf() ), groupInfo = GroupInfo.sampleData, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt index c32b52914e..3ec6d1ed38 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt @@ -66,11 +66,9 @@ fun GroupMemberInfoView( withApi { val c = chatModel.controller.apiGetChat(ChatType.Direct, it) if (c != null) { - // TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend - val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected())) - chatModel.addChat(newChat) + chatModel.addChat(c) chatModel.chatItems.clear() - chatModel.chatId.value = newChat.id + chatModel.chatId.value = c.id closeAll() } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index ff88b3762b..0a6fd54463 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -44,17 +44,19 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { delay(500L) } when (chat.chatInfo) { - is ChatInfo.Direct -> + is ChatInfo.Direct -> { + val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, stopped ) + } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, showMenu, @@ -627,6 +629,7 @@ fun PreviewChatListNavLinkDirect() { ), false, null, + null, stopped = false, linkMode = SimplexLinkMode.DESCRIPTION ) @@ -665,6 +668,7 @@ fun PreviewChatListNavLinkGroup() { ), false, null, + null, stopped = false, linkMode = SimplexLinkMode.DESCRIPTION ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 7bdc675a6d..a385efda46 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -25,7 +25,14 @@ import chat.simplex.app.views.chat.item.MarkdownText import chat.simplex.app.views.helpers.* @Composable -fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean, linkMode: SimplexLinkMode) { +fun ChatPreviewView( + chat: Chat, + chatModelIncognito: Boolean, + currentUserProfileDisplayName: String?, + contactNetworkStatus: NetworkStatus?, + stopped: Boolean, + linkMode: SimplexLinkMode +) { val cInfo = chat.chatInfo @Composable @@ -187,7 +194,9 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD Modifier.padding(top = 52.dp), contentAlignment = Alignment.Center ) { - ChatStatusImage(chat) + if (chat.chatInfo is ChatInfo.Direct) { + ChatStatusImage(chat, contactNetworkStatus) + } } } } @@ -210,10 +219,9 @@ fun unreadCountStr(n: Int): String { } @Composable -fun ChatStatusImage(chat: Chat) { - val s = chat.serverInfo.networkStatus - val descr = s.statusString - if (s is Chat.NetworkStatus.Error) { +fun ChatStatusImage(chat: Chat, s: NetworkStatus?) { + val descr = s?.statusString + if (s is NetworkStatus.Error) { Icon( Icons.Outlined.ErrorOutline, contentDescription = descr, @@ -221,7 +229,7 @@ fun ChatStatusImage(chat: Chat) { modifier = Modifier .size(19.dp) ) - } else if (s !is Chat.NetworkStatus.Connected) { + } else if (s !is NetworkStatus.Connected) { CircularProgressIndicator( Modifier .padding(horizontal = 2.dp) @@ -241,6 +249,6 @@ fun ChatStatusImage(chat: Chat) { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, false, "", stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) + ChatPreviewView(Chat.sampleData, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt index b1de4d8d5f..113e454b1f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt @@ -29,12 +29,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences()) val updatedProfile = m.controller.apiUpdateProfile(newProfile) if (updatedProfile != null) { - val updatedUser = user.copy( - profile = updatedProfile.toLocalProfile(user.profile.profileId), - fullPreferences = preferences - ) + m.updateCurrentUser(updatedProfile, preferences) currentPreferences = preferences - m.currentUser.value = updatedUser } afterSave() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt index b33a9548c5..3baa54ea34 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -46,9 +46,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { withApi { val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) if (newProfile != null) { - chatModel.currentUser.value?.profile?.profileId?.let { - chatModel.updateUserProfile(newProfile.toLocalProfile(it)) - } + chatModel.updateCurrentUser(newProfile) profile = newProfile } editProfile.value = false