Switch MAX bridge to PyMax only
This commit is contained in:
@@ -164,6 +164,7 @@ import kotlinx.coroutines.withContext
|
||||
import xyz.kusoft.qmax.core.push.ForegroundChatTracker
|
||||
import xyz.kusoft.qmax.core.model.AttachmentDto
|
||||
import xyz.kusoft.qmax.core.model.ChatDto
|
||||
import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
import xyz.kusoft.qmax.core.model.QMaxSession
|
||||
import xyz.kusoft.qmax.ui.QMaxUiState
|
||||
@@ -296,7 +297,7 @@ private fun LoginScreen(vm: QMaxViewModel) {
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("QMAX", style = MaterialTheme.typography.displaySmall, fontWeight = FontWeight.Bold, color = QMaxBlue)
|
||||
Text("MAX Web в привычном мессенджере", color = QMaxMuted, modifier = Modifier.padding(top = 6.dp))
|
||||
Text("MAX в привычном мессенджере", color = QMaxMuted, modifier = Modifier.padding(top = 6.dp))
|
||||
Spacer(Modifier.height(28.dp))
|
||||
OutlinedTextField(
|
||||
value = state.serverUrl,
|
||||
@@ -370,6 +371,10 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
onMenuOpenChange = { menuOpen = it },
|
||||
onRefresh = vm::loadChats,
|
||||
onCheckUpdates = vm::checkArgusUpdate,
|
||||
onRefreshMaxStatus = vm::loadMaxStatus,
|
||||
onStartMaxLogin = vm::startMaxLogin,
|
||||
onMaxCode = vm::updateMaxCode,
|
||||
onSubmitMaxCode = vm::submitMaxCode,
|
||||
onLogout = vm::logout,
|
||||
onSearch = vm::updateSearchQuery,
|
||||
onTabSelected = { tab ->
|
||||
@@ -596,6 +601,10 @@ private fun TelegramChatListContent(
|
||||
onMenuOpenChange: (Boolean) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onCheckUpdates: () -> Unit,
|
||||
onRefreshMaxStatus: () -> Unit,
|
||||
onStartMaxLogin: () -> Unit,
|
||||
onMaxCode: (String) -> Unit,
|
||||
onSubmitMaxCode: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onTabSelected: (MainTab) -> Unit,
|
||||
@@ -648,10 +657,17 @@ private fun TelegramChatListContent(
|
||||
)
|
||||
}
|
||||
ErrorLine(state.error)
|
||||
if (activeTab != MainTab.Settings && state.maxStatus?.isAuthorized == false) {
|
||||
MaxSessionExpiredBanner(onClick = { onTabSelected(MainTab.Settings) })
|
||||
}
|
||||
if (activeTab == MainTab.Settings) {
|
||||
SettingsTabContent(
|
||||
state = state,
|
||||
onCheckUpdates = onCheckUpdates,
|
||||
onRefreshMaxStatus = onRefreshMaxStatus,
|
||||
onStartMaxLogin = onStartMaxLogin,
|
||||
onMaxCode = onMaxCode,
|
||||
onSubmitMaxCode = onSubmitMaxCode,
|
||||
onRefresh = onRefresh,
|
||||
onLogout = onLogout
|
||||
)
|
||||
@@ -1039,10 +1055,29 @@ private fun ChatListFloatingActions(modifier: Modifier = Modifier, onNewChat: ()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MaxSessionExpiredBanner(onClick: () -> Unit) {
|
||||
Surface(
|
||||
color = Color(0xFFFFF3E0),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
Column(Modifier.padding(horizontal = 20.dp, vertical = 10.dp)) {
|
||||
Text("Сессия MAX закончилась", fontWeight = FontWeight.SemiBold, color = Color(0xFF9A4D00))
|
||||
Text("Откройте настройки, получите новый код и отдайте его QMAX.", color = QMaxMuted, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsTabContent(
|
||||
state: QMaxUiState,
|
||||
onCheckUpdates: () -> Unit,
|
||||
onRefreshMaxStatus: () -> Unit,
|
||||
onStartMaxLogin: () -> Unit,
|
||||
onMaxCode: (String) -> Unit,
|
||||
onSubmitMaxCode: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onLogout: () -> Unit
|
||||
) {
|
||||
@@ -1066,19 +1101,67 @@ private fun SettingsTabContent(
|
||||
HorizontalDivider(color = Color(0xFFE7EEF3))
|
||||
}
|
||||
item {
|
||||
val status = state.maxStatus
|
||||
val loginAvailable = status?.isAuthorized == false
|
||||
val waitingForCode = isMaxLoginWaitingForCode(status)
|
||||
val requestCodeEnabled = loginAvailable && !waitingForCode && !state.loading
|
||||
val submitCodeEnabled = loginAvailable && state.maxCode.trim().isNotBlank() && !state.loading
|
||||
Spacer(Modifier.height(22.dp))
|
||||
Text("MAX", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = QMaxText)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
state.maxStatus?.status ?: "Статус неизвестен",
|
||||
maxStatusTitle(status),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = QMaxMuted
|
||||
color = if (loginAvailable) Color(0xFFC62828) else QMaxMuted
|
||||
)
|
||||
state.maxStatus?.lastError?.takeIf { it.isNotBlank() }?.let {
|
||||
Text(
|
||||
maxStatusHint(status),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = QMaxMuted,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
status?.lastError?.takeIf { it.isNotBlank() }?.let {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(it, style = MaterialTheme.typography.bodyMedium, color = Color(0xFFC62828))
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Column {
|
||||
Button(
|
||||
onClick = onRefreshMaxStatus,
|
||||
enabled = !state.loading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Обновить статус")
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = onStartMaxLogin,
|
||||
enabled = requestCodeEnabled,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(if (waitingForCode) "Код запрошен" else "Получить новый код")
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
OutlinedTextField(
|
||||
value = state.maxCode,
|
||||
onValueChange = onMaxCode,
|
||||
enabled = loginAvailable && !state.loading,
|
||||
label = { Text("Новый код MAX") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = onSubmitMaxCode,
|
||||
enabled = submitCodeEnabled,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Отдать код")
|
||||
}
|
||||
Spacer(Modifier.height(22.dp))
|
||||
HorizontalDivider(color = Color(0xFFE7EEF3))
|
||||
Spacer(Modifier.height(22.dp))
|
||||
Button(onClick = onRefresh, enabled = !state.loading) {
|
||||
Text("Обновить чаты")
|
||||
}
|
||||
@@ -1090,6 +1173,31 @@ private fun SettingsTabContent(
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMaxLoginWaitingForCode(status: MaxBridgeStatusDto?): Boolean {
|
||||
return status?.loginStage.equals("Code", ignoreCase = true) ||
|
||||
status?.status.equals("Code", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun maxStatusTitle(status: MaxBridgeStatusDto?): String {
|
||||
return when {
|
||||
status == null -> "Статус неизвестен"
|
||||
status.isAuthorized -> "Сессия MAX активна"
|
||||
status.loginStage.equals("Expired", ignoreCase = true) ||
|
||||
status.status.equals("SessionExpired", ignoreCase = true) -> "Сессия MAX закончилась"
|
||||
isMaxLoginWaitingForCode(status) -> "MAX ждёт код подтверждения"
|
||||
else -> "MAX требует вход"
|
||||
}
|
||||
}
|
||||
|
||||
private fun maxStatusHint(status: MaxBridgeStatusDto?): String {
|
||||
return when {
|
||||
status == null -> "Проверка статуса выполняется автоматически. Можно обновить статус вручную."
|
||||
status.isAuthorized -> "Получение нового кода будет доступно только после окончания сессии."
|
||||
isMaxLoginWaitingForCode(status) -> "Введите код из MAX и нажмите «Отдать код»."
|
||||
else -> "Нажмите «Получить новый код», затем введите код подтверждения."
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TelegramBottomNavigation(
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDeletedDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
import xyz.kusoft.qmax.core.model.QMaxSession
|
||||
@@ -26,7 +27,8 @@ class QMaxRealtimeClient {
|
||||
onChatListInvalidated: () -> Unit,
|
||||
onMessageCreated: (MessageDto) -> Unit,
|
||||
onMessageUpdated: (MessageDto) -> Unit,
|
||||
onMessageDeleted: (MessageDeletedDto) -> Unit
|
||||
onMessageDeleted: (MessageDeletedDto) -> Unit,
|
||||
onMaxStatusChanged: (MaxBridgeStatusDto) -> Unit
|
||||
) = withContext(Dispatchers.IO) {
|
||||
disconnect()
|
||||
|
||||
@@ -36,6 +38,15 @@ class QMaxRealtimeClient {
|
||||
.build()
|
||||
|
||||
connection.on("ChatListInvalidated", onChatListInvalidated)
|
||||
connection.on(
|
||||
"MaxStatusChanged",
|
||||
{ payload: JsonElement ->
|
||||
runCatching {
|
||||
onMaxStatusChanged(json.decodeFromString<MaxBridgeStatusDto>(payload.toString()))
|
||||
}
|
||||
},
|
||||
JsonElement::class.java
|
||||
)
|
||||
connection.on(
|
||||
"MessageCreated",
|
||||
{ payload: JsonElement ->
|
||||
|
||||
@@ -72,6 +72,7 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
private var chatRefreshJob: Job? = null
|
||||
private var chatRefreshGeneration = 0L
|
||||
private var chatSearchJob: Job? = null
|
||||
private var maxStatusPollJob: Job? = null
|
||||
private var autoLoginJob: Job? = null
|
||||
private var pushRegisteredForToken: String? = null
|
||||
private var pushRegistrationJob: Job? = null
|
||||
@@ -107,11 +108,12 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
loadChats()
|
||||
connectRealtime(session)
|
||||
registerPushDevice(session)
|
||||
loadMaxStatus()
|
||||
startMaxStatusPolling(session)
|
||||
} else {
|
||||
realtimeConnectJob?.cancel()
|
||||
messagePollJob?.cancel()
|
||||
presencePollJob?.cancel()
|
||||
maxStatusPollJob?.cancel()
|
||||
pushRegistrationJob?.cancel()
|
||||
pushRegistrationInFlightForToken = null
|
||||
pushRegisteredForToken = null
|
||||
@@ -687,19 +689,20 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
|
||||
fun loadMaxStatus() = launchLoading(false) {
|
||||
val session = requireSession()
|
||||
state.value = state.value.copy(maxStatus = repository.maxStatus(session))
|
||||
refreshMaxStatus(session)
|
||||
}
|
||||
|
||||
fun startMaxLogin() = launchLoading {
|
||||
val session = requireSession()
|
||||
state.value = state.value.copy(maxStatus = repository.startMaxLogin(session, null))
|
||||
applyMaxStatus(repository.startMaxLogin(session, null))
|
||||
}
|
||||
|
||||
fun submitMaxCode() = launchLoading {
|
||||
val session = requireSession()
|
||||
val code = state.value.maxCode.trim()
|
||||
if (code.isBlank()) return@launchLoading
|
||||
state.value = state.value.copy(maxStatus = repository.submitMaxCode(session, code), maxCode = "")
|
||||
applyMaxStatus(repository.submitMaxCode(session, code))
|
||||
state.value = state.value.copy(maxCode = "")
|
||||
}
|
||||
|
||||
fun checkArgusUpdate() = launchLoading {
|
||||
@@ -1044,6 +1047,28 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMaxStatusPolling(session: QMaxSession) {
|
||||
maxStatusPollJob?.cancel()
|
||||
maxStatusPollJob = viewModelScope.launch {
|
||||
while (true) {
|
||||
runCatching {
|
||||
refreshMaxStatus(session)
|
||||
}.onFailure {
|
||||
Log.w(LogTag, "MAX status refresh failed", it)
|
||||
}
|
||||
delay(MaxStatusPollIntervalMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshMaxStatus(session: QMaxSession) {
|
||||
applyMaxStatus(repository.maxStatus(session))
|
||||
}
|
||||
|
||||
private fun applyMaxStatus(status: MaxBridgeStatusDto) {
|
||||
state.value = state.value.copy(maxStatus = status)
|
||||
}
|
||||
|
||||
private fun connectRealtime(session: QMaxSession) {
|
||||
realtimeConnectJob?.cancel()
|
||||
realtimeConnectJob = viewModelScope.launch {
|
||||
@@ -1053,7 +1078,8 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
onChatListInvalidated = ::scheduleChatListRefresh,
|
||||
onMessageCreated = ::handleRealtimeMessage,
|
||||
onMessageUpdated = ::handleRealtimeMessageUpdated,
|
||||
onMessageDeleted = ::handleRealtimeMessageDeleted
|
||||
onMessageDeleted = ::handleRealtimeMessageDeleted,
|
||||
onMaxStatusChanged = ::applyMaxStatus
|
||||
)
|
||||
realtime.joinChat(state.value.selectedChat?.id)
|
||||
}
|
||||
@@ -1245,6 +1271,7 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
const val ChatListTimeoutMs = 45_000L
|
||||
const val MessageSyncTimeoutMs = 120_000L
|
||||
const val MessageHydrationIntervalMs = 120_000L
|
||||
const val MaxStatusPollIntervalMs = 60_000L
|
||||
const val AutoLoginDelayMs = 1_000L
|
||||
const val PushRegistrationAttempts = 6
|
||||
const val InitialPushRegistrationRetryMs = 15_000L
|
||||
|
||||
Reference in New Issue
Block a user