Событие печати, смена адреса, иконки
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
|
<application
|
||||||
android:name="com.knot.messenger.MainApplication"
|
android:name="com.knot.messenger.MainApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.KnotMessenger">
|
android:theme="@style/Theme.KnotMessenger"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:usesCleartextTraffic="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.knot.messenger.MainActivity"
|
android:name="com.knot.messenger.MainActivity"
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
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 ?: ""))
|
_events.tryEmit(ChatEvent.UserTyping(data.chatId ?: "", data.userId ?: ""))
|
||||||
}, ReactionEvent::class.java)
|
}, 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 ->
|
conn.on("user_online", { userId: String ->
|
||||||
_events.tryEmit(ChatEvent.UserOnline(userId))
|
_events.tryEmit(ChatEvent.UserOnline(userId))
|
||||||
}, String::class.java)
|
}, 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
|
// Ensure SignalR is connected and join the chat room
|
||||||
val token = tokenManager.getToken()
|
val token = tokenManager.getToken()
|
||||||
if (token != null) {
|
val baseUrl = serverConfig.getBaseUrl()
|
||||||
val baseUrl = serverConfig.getBaseUrl()
|
if (token != null && baseUrl.isNotBlank()) {
|
||||||
signalrClient.connect(baseUrl, token)
|
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||||||
signalrClient.joinChat(chatId)
|
signalrClient.joinChat(chatId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +230,9 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
_state.update { it.copy(isTyping = false) }
|
_state.update { it.copy(isTyping = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is ChatEvent.UserStoppedTyping -> {
|
||||||
|
_state.update { it.copy(isTyping = false) }
|
||||||
|
}
|
||||||
is ChatEvent.MessagesRead -> {
|
is ChatEvent.MessagesRead -> {
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
val updatedMessages = currentState.messages.map { msg ->
|
val updatedMessages = currentState.messages.map { msg ->
|
||||||
@@ -285,6 +288,22 @@ class ChatDetailViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun onInputTextChanged(text: String) {
|
fun onInputTextChanged(text: String) {
|
||||||
_state.update { it.copy(inputText = text) }
|
_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 = {}) {
|
fun sendMessage(onFail: (String) -> Unit = {}) {
|
||||||
|
|||||||
@@ -44,9 +44,11 @@ class ChatListViewModel @Inject constructor(
|
|||||||
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
_state.update { it.copy(isStoriesEnabled = isStoriesEnabled) }
|
||||||
|
|
||||||
val token = tokenManager.getToken()
|
val token = tokenManager.getToken()
|
||||||
if (token != null) {
|
val baseUrl = serverConfig.getBaseUrl()
|
||||||
val baseUrl = serverConfig.getBaseUrl().removeSuffix("/api/")
|
|
||||||
signalrClient.connect(baseUrl, token)
|
// Подключаемся к SignalR только если есть токен И введён URL сервера
|
||||||
|
if (token != null && baseUrl.isNotBlank()) {
|
||||||
|
signalrClient.connect(baseUrl.removeSuffix("/api/"), token)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadChats()
|
loadChats()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package core.network
|
package core.network
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import core.domain.model.FeaturesConfig
|
import core.domain.model.FeaturesConfig
|
||||||
import core.domain.model.ServerConfigModel
|
import core.domain.model.ServerConfigModel
|
||||||
@@ -13,12 +12,12 @@ class ServerConfig @Inject constructor(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val gson: Gson
|
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 {
|
companion object {
|
||||||
const val DEFAULT_BASE_URL = "https://your-api.com/api/"
|
|
||||||
private const val KEY_BASE_URL = "base_url"
|
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 {
|
fun getBaseUrl(): String {
|
||||||
@@ -48,3 +47,4 @@ class ServerConfig @Inject constructor(
|
|||||||
return feature(getServerConfig().features)
|
return feature(getServerConfig().features)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,7 @@ fun SettingsScreen(
|
|||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
var baseUrl by remember(state.baseUrl) { mutableStateOf(state.baseUrl) }
|
var baseUrl by remember(state.baseUrl) { mutableStateOf(state.baseUrl) }
|
||||||
|
|
||||||
val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current
|
// Убран автоматический вызов loadConfig() - теперь только по кнопке Refresh
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = Color(0xFF0F0F10),
|
containerColor = Color(0xFF0F0F10),
|
||||||
@@ -110,7 +99,9 @@ fun SettingsScreen(
|
|||||||
focusedIndicatorColor = Color(0xFF3390EC),
|
focusedIndicatorColor = Color(0xFF3390EC),
|
||||||
unfocusedIndicatorColor = Color.Gray.copy(alpha = 0.5f)
|
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) }) {
|
IconButton(onClick = { viewModel.saveBaseUrl(baseUrl) }) {
|
||||||
Icon(Icons.Default.Save, contentDescription = null, tint = Color(0xFF3390EC))
|
Icon(Icons.Default.Save, contentDescription = null, tint = Color(0xFF3390EC))
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
refreshState()
|
refreshState()
|
||||||
loadConfig()
|
// loadConfig() убран - вызывается только по кнопке Refresh или если URL уже введён
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshState() {
|
private fun refreshState() {
|
||||||
@@ -44,6 +44,13 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadConfig() {
|
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 {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(isLoading = true, error = null) }
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
val result = authRepository.fetchConfig()
|
val result = authRepository.fetchConfig()
|
||||||
@@ -58,6 +65,16 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveBaseUrl(url: String) {
|
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)
|
serverConfig.setBaseUrl(url)
|
||||||
_state.update { it.copy(baseUrl = url) }
|
_state.update { it.copy(baseUrl = url) }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user