diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 37baed40b2..71ea873ddd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -423,6 +423,16 @@ object ChatController { fun hasChatCtrl() = ctrl != -1L && ctrl != null + suspend fun getAgentSubsTotal(rh: Long?): Pair? { + val userId = currentUserId("getAgentSubsTotal") + + val r = sendCmd(rh, CC.GetAgentSubsTotal(userId), log = false) + + if (r is CR.AgentSubsTotal) return r.subsTotal to r.hasSession + Log.e(TAG, "getAgentSubsTotal bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? { val userId = currentUserId("getAgentServersSummary") @@ -2815,6 +2825,7 @@ sealed class CC { // misc class ShowVersion(): CC() class ResetAgentServersStats(): CC() + class GetAgentSubsTotal(val userId: Long): CC() class GetAgentServersSummary(val userId: Long): CC() val cmdString: String get() = when (this) { @@ -2975,6 +2986,7 @@ sealed class CC { is ApiStandaloneFileInfo -> "/_download info $url" is ShowVersion -> "/version" is ResetAgentServersStats -> "/reset servers stats" + is GetAgentSubsTotal -> "/get subs total $userId" is GetAgentServersSummary -> "/get servers summary $userId" } @@ -3108,6 +3120,7 @@ sealed class CC { is ApiStandaloneFileInfo -> "apiStandaloneFileInfo" is ShowVersion -> "showVersion" is ResetAgentServersStats -> "resetAgentServersStats" + is GetAgentSubsTotal -> "getAgentSubsTotal" is GetAgentServersSummary -> "getAgentServersSummary" } @@ -3638,6 +3651,9 @@ data class ServerSessions( ssConnecting = 0 ) } + + val hasSess: Boolean + get() = ssConnected > 0 } @Serializable @@ -4743,6 +4759,7 @@ sealed class CR { @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR() + @Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR() @Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR() // general @Serializable class Response(val type: String, val json: String): CR() @@ -4904,6 +4921,7 @@ sealed class CR { is ContactPQAllowed -> "contactPQAllowed" is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" + is AgentSubsTotal -> "agentSubsTotal" is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" @@ -5084,6 +5102,7 @@ sealed class CR { is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason" is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption") is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled") + is AgentSubsTotal -> withUser(user, "subsTotal: ${subsTotal}\nhasSession: $hasSession") is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary)) is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 5759877b07..43f0b7ef9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -255,7 +255,6 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt fontWeight = FontWeight.SemiBold, ) SubscriptionStatusIndicator( - serversSummary = serversSummary, click = { ModalManager.start.closeModals() ModalManager.start.showModalCloseable( @@ -286,34 +285,33 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt } @Composable -fun SubscriptionStatusIndicator(serversSummary: MutableState, click: (() -> Unit)) { +fun SubscriptionStatusIndicator(click: (() -> Unit)) { var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } - var sess by remember { mutableStateOf(ServerSessions.newServerSessions) } + var hasSess by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() - suspend fun setServersSummary() { - serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) - - serversSummary.value?.let { - subs = it.allUsersSMP.smpTotals.subs - sess = it.allUsersSMP.smpTotals.sessions + suspend fun setSubsTotal() { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second } } LaunchedEffect(Unit) { - setServersSummary() + setSubsTotal() scope.launch { while (isActive) { delay(1.seconds) if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { - setServersSummary() + setSubsTotal() } } } } SimpleButtonFrame(click = click) { - SubscriptionStatusIndicatorView(subs = subs, sess = sess) + SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index 74f4fcdcc3..8621c73ef3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -45,8 +45,7 @@ import chat.simplex.common.model.ServerProtocol import chat.simplex.common.model.ServerSessions import chat.simplex.common.model.XFTPServerSummary import chat.simplex.common.model.localTimestamp -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.ProtocolServersView @@ -54,12 +53,13 @@ import chat.simplex.common.views.usersettings.SettingsPreferenceItem import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.datetime.Instant import numOrDash import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds enum class SubscriptionColorType { ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED @@ -76,7 +76,7 @@ fun subscriptionStatusColorAndPercentage( online: Boolean, socksProxy: String?, subs: SMPServerSubs, - sess: ServerSessions + hasSess: Boolean ): SubscriptionStatus { fun roundedToQuarter(n: Float): Float = when { @@ -91,16 +91,16 @@ fun subscriptionStatusColorAndPercentage( return if (online && subs.total > 0) { if (subs.ssActive == 0) { - if (sess.ssConnected == 0) - noConnColorAndPercent - else + if (hasSess) SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + else + noConnColorAndPercent } else { // ssActive > 0 - if (sess.ssConnected == 0) + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + else // This would mean implementation error SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) - else - SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) } } else noConnColorAndPercent } @@ -116,9 +116,9 @@ private fun SubscriptionStatusIndicatorPercentage(percentageText: String) { } @Composable -fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, sess: ServerSessions, leadingPercentage: Boolean = false) { +fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadingPercentage: Boolean = false) { val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) - val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, sess) + val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess) val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage } val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" @@ -193,7 +193,7 @@ private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh ) if (srvSumm.subs != null) { Spacer(Modifier.fillMaxWidth().weight(1f)) - SubscriptionStatusIndicatorView(subs = srvSumm.subs, sess = srvSumm.sessionsOrNew, leadingPercentage = true) + SubscriptionStatusIndicatorView(subs = srvSumm.subs, hasSess = srvSumm.sessionsOrNew.hasSess, leadingPercentage = true) } else if (srvSumm.sessions != null) { Spacer(Modifier.fillMaxWidth().weight(1f)) Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions)) @@ -334,7 +334,7 @@ private fun SMPSubscriptionsSection(totals: SMPTotals) { style = MaterialTheme.typography.body2, fontSize = 12.sp ) - SubscriptionStatusIndicatorView(totals.subs, totals.sessions) + SubscriptionStatusIndicatorView(totals.subs, totals.sessions.hasSess) } Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { InfoRow( @@ -364,7 +364,7 @@ private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSumma style = MaterialTheme.typography.body2, fontSize = 12.sp ) - SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew) + SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess) } Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { InfoRow( @@ -718,6 +718,10 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } } val scope = rememberCoroutineScope() + suspend fun setServersSummary() { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + } + LaunchedEffect(Unit) { if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 ) { @@ -725,6 +729,29 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta } else { showUserSelection = true } + setServersSummary() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chat.simplex.common.platform.chatModel.chatId.value == null) && isAppVisibleAndFocused()) { + setServersSummary() + } + } + } + } + + fun resetStats() { + withBGApi { + val success = controller.resetAgentServersStats(rh?.remoteHostId) + if (success) { + setServersSummary() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) + ) + } + } } Column( @@ -908,7 +935,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta SectionView { ReconnectAllServersButton(rh) - ResetStatisticsButton(rh) + ResetStatisticsButton(rh, resetStats = { resetStats() }) } SectionBottomSpacer() @@ -949,8 +976,8 @@ private fun reconnectAllServersAlert(rh: RemoteHostInfo?) { } @Composable -private fun ResetStatisticsButton(rh: RemoteHostInfo?) { - SectionItemView(click = { resetStatisticsAlert(rh) }) { +private fun ResetStatisticsButton(rh: RemoteHostInfo?, resetStats: () -> Unit) { + SectionItemView(click = { resetStatisticsAlert(rh, resetStats) }) { Text( stringResource(MR.strings.servers_info_reset_stats), color = MaterialTheme.colors.primary @@ -958,23 +985,12 @@ private fun ResetStatisticsButton(rh: RemoteHostInfo?) { } } -private fun resetStatisticsAlert(rh: RemoteHostInfo?) { +private fun resetStatisticsAlert(rh: RemoteHostInfo?, resetStats: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.servers_info_reset_stats_alert_title), text = generalGetString(MR.strings.servers_info_reset_stats_alert_message), confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm), destructive = true, - onConfirm = { - withBGApi { - val success = controller.resetAgentServersStats(rh?.remoteHostId) - - if (!success) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.servers_info_modal_error_title), - text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) - ) - } - } - } + onConfirm = resetStats ) }