Событие печати, смена адреса, иконки
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,9 +11,12 @@
|
||||
<application
|
||||
android:name="com.knot.messenger.MainApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.KnotMessenger">
|
||||
android:theme="@style/Theme.KnotMessenger"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name="com.knot.messenger.MainActivity"
|
||||
|
||||
BIN
client-mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
client-mobile/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
client-mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
client-mobile/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
client-mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
client-mobile/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
client-mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
client-mobile/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
BIN
client-mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
client-mobile/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Для отладки: доверяем пользовательским сертификатам -->
|
||||
<debug-overrides>
|
||||
<trust-anchors>
|
||||
<certificates src="user" />
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</debug-overrides>
|
||||
|
||||
<!-- Разрешаем cleartext (HTTP) трафик для локальных IP -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
<domain includeSubdomains="true">10.0.0.0/8</domain>
|
||||
<domain includeSubdomains="true">172.16.0.0/12</domain>
|
||||
<domain includeSubdomains="true">192.168.0.0/16</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
@@ -107,6 +107,10 @@ class ChatHubClient @Inject constructor() {
|
||||
_events.tryEmit(ChatEvent.UserTyping(data.chatId ?: "", data.userId ?: ""))
|
||||
}, ReactionEvent::class.java)
|
||||
|
||||
conn.on("user_stopped_typing", { data: ReactionEvent ->
|
||||
_events.tryEmit(ChatEvent.UserStoppedTyping(data.chatId ?: "", data.userId ?: ""))
|
||||
}, ReactionEvent::class.java)
|
||||
|
||||
conn.on("user_online", { userId: String ->
|
||||
_events.tryEmit(ChatEvent.UserOnline(userId))
|
||||
}, String::class.java)
|
||||
@@ -181,4 +185,18 @@ class ChatHubClient @Inject constructor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendTypingIndicator(chatId: String) {
|
||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
||||
hubConnection?.send("typing_start", chatId)
|
||||
Log.d("ChatHubClient", "Sent typing indicator for chat: $chatId")
|
||||
}
|
||||
}
|
||||
|
||||
fun sendUserStoppedTyping(chatId: String) {
|
||||
if (hubConnection?.connectionState == HubConnectionState.CONNECTED) {
|
||||
hubConnection?.send("typing_stop", chatId)
|
||||
Log.d("ChatHubClient", "Sent user stopped typing for chat: $chatId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +100,9 @@ class ChatDetailViewModel @Inject constructor(
|
||||
|
||||
// Ensure SignalR is connected and join the chat room
|
||||
val token = tokenManager.getToken()
|
||||
if (token != null) {
|
||||
val baseUrl = serverConfig.getBaseUrl()
|
||||
signalrClient.connect(baseUrl, token)
|
||||
val baseUrl = serverConfig.getBaseUrl()
|
||||
if (token != null && baseUrl.isNotBlank()) {
|
||||
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||||
signalrClient.joinChat(chatId)
|
||||
}
|
||||
|
||||
@@ -230,6 +230,9 @@ class ChatDetailViewModel @Inject constructor(
|
||||
_state.update { it.copy(isTyping = false) }
|
||||
}
|
||||
}
|
||||
is ChatEvent.UserStoppedTyping -> {
|
||||
_state.update { it.copy(isTyping = false) }
|
||||
}
|
||||
is ChatEvent.MessagesRead -> {
|
||||
_state.update { currentState ->
|
||||
val updatedMessages = currentState.messages.map { msg ->
|
||||
@@ -285,6 +288,22 @@ class ChatDetailViewModel @Inject constructor(
|
||||
|
||||
fun onInputTextChanged(text: String) {
|
||||
_state.update { it.copy(inputText = text) }
|
||||
|
||||
val chatId = currentChatId ?: return
|
||||
|
||||
// Отправляем индикатор набора текста
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastTypingSentTime > 1000) {
|
||||
signalrClient.sendTypingIndicator(chatId)
|
||||
lastTypingSentTime = now
|
||||
|
||||
// Отправляем "остановился печатать" через 3 секунды
|
||||
typingTimerJob?.cancel()
|
||||
typingTimerJob = viewModelScope.launch {
|
||||
delay(3000)
|
||||
signalrClient.sendUserStoppedTyping(chatId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(onFail: (String) -> Unit = {}) {
|
||||
@@ -298,7 +317,7 @@ class ChatDetailViewModel @Inject constructor(
|
||||
|
||||
val tempId = "temp_${System.currentTimeMillis()}"
|
||||
val userId = getCurrentUserId()
|
||||
|
||||
|
||||
// Determine mediaType based on attachments
|
||||
val mediaType = when {
|
||||
pending.isEmpty() -> chats.domain.model.MediaType.TEXT
|
||||
|
||||
@@ -44,9 +44,11 @@ class ChatListViewModel @Inject constructor(
|
||||
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
||||
|
||||
val token = tokenManager.getToken()
|
||||
if (token != null) {
|
||||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
||||
signalrClient.connect(baseUrl, token)
|
||||
val baseUrl = serverConfig.getBaseUrl()
|
||||
|
||||
// Подключаемся к SignalR только если есть токен И введён URL сервера
|
||||
if (token != null && baseUrl.isNotBlank()) {
|
||||
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||||
}
|
||||
|
||||
loadChats()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package core.network
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.google.gson.Gson
|
||||
import core.domain.model.FeaturesConfig
|
||||
import core.domain.model.ServerConfigModel
|
||||
@@ -13,12 +12,12 @@ class ServerConfig @Inject constructor(
|
||||
private val context: Context,
|
||||
private val gson: Gson
|
||||
) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("server_settings", Context.MODE_PRIVATE)
|
||||
private val prefs = context.getSharedPreferences("server_config_prefs", Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_BASE_URL = "https://your-api.com/api/"
|
||||
private const val KEY_BASE_URL = "base_url"
|
||||
private const val KEY_CONFIG_DATA = "config_data"
|
||||
private const val KEY_CONFIG_DATA = "server_config_data"
|
||||
private const val DEFAULT_BASE_URL = "" // Пустой по умолчанию
|
||||
}
|
||||
|
||||
fun getBaseUrl(): String {
|
||||
@@ -48,3 +47,4 @@ class ServerConfig @Inject constructor(
|
||||
return feature(getServerConfig().features)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,18 +32,7 @@ fun SettingsScreen(
|
||||
val state by viewModel.state.collectAsState()
|
||||
var baseUrl by remember(state.baseUrl) { mutableStateOf(state.baseUrl) }
|
||||
|
||||
val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = androidx.lifecycle.LifecycleEventObserver { _, event ->
|
||||
if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.loadConfig()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
// Убран автоматический вызов loadConfig() - теперь только по кнопке Refresh
|
||||
|
||||
Scaffold(
|
||||
containerColor = Color(0xFF0F0F10),
|
||||
@@ -110,7 +99,9 @@ fun SettingsScreen(
|
||||
focusedIndicatorColor = Color(0xFF3390EC),
|
||||
unfocusedIndicatorColor = Color.Gray.copy(alpha = 0.5f)
|
||||
),
|
||||
placeholder = { Text("https://...", color = Color.Gray) }
|
||||
placeholder = {
|
||||
Text("Например: http://192.168.1.100:5034/api/", color = Color.Gray.copy(alpha = 0.5f))
|
||||
}
|
||||
)
|
||||
IconButton(onClick = { viewModel.saveBaseUrl(baseUrl) }) {
|
||||
Icon(Icons.Default.Save, contentDescription = null, tint = Color(0xFF3390EC))
|
||||
|
||||
@@ -31,7 +31,7 @@ class SettingsViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
refreshState()
|
||||
loadConfig()
|
||||
// loadConfig() убран - вызывается только по кнопке Refresh или если URL уже введён
|
||||
}
|
||||
|
||||
private fun refreshState() {
|
||||
@@ -44,6 +44,13 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun loadConfig() {
|
||||
// Не загружаем конфиг, если URL не введён
|
||||
val currentUrl = serverConfig.getBaseUrl()
|
||||
if (currentUrl.isBlank()) {
|
||||
android.util.Log.d("SettingsViewModel", "Base URL is empty, skipping config load")
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
val result = authRepository.fetchConfig()
|
||||
@@ -58,6 +65,16 @@ class SettingsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun saveBaseUrl(url: String) {
|
||||
val currentUrl = serverConfig.getBaseUrl()
|
||||
|
||||
// Если URL действительно изменился, очищаем токен и логируем
|
||||
if (currentUrl != url) {
|
||||
android.util.Log.d("SettingsViewModel", "Server URL changed: $currentUrl -> $url, clearing token")
|
||||
viewModelScope.launch {
|
||||
authRepository.logout()
|
||||
}
|
||||
}
|
||||
|
||||
serverConfig.setBaseUrl(url)
|
||||
_state.update { it.copy(baseUrl = url) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user