diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index ba29ab3..44dc092 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -32,6 +32,12 @@
+
+
+
+
+
+
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt
index 344c3ed..f0aa5f6 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt
@@ -176,6 +176,24 @@ object ApiClient {
}
.body()
+ suspend fun checkSimilarity(userId: Int): SimilarityResult? =
+ runCatching {
+ http
+ .get("${Config.apiBaseUrl}/user/check-similarity/$userId") {
+ contentType(ContentType.Application.Json)
+ }
+ .body()
+ }.getOrNull()
+
+ suspend fun verifyUser(userId: Int): VerifyResponse? =
+ runCatching {
+ http
+ .post("${Config.apiBaseUrl}/user/$userId/verify") {
+ contentType(ContentType.Application.Json)
+ }
+ .body()
+ }.getOrNull()
+
suspend fun getDmConversations(): List =
http
.get("${Config.apiBaseUrl}/dm/conversations") {
@@ -429,4 +447,64 @@ object ApiClient {
)
}
}
+
+ suspend fun sendDmTyping(recipientId: Int) {
+ runCatching {
+ WebSocketManager.send(
+ WebSocketMessage(
+ type = "dmTyping",
+ credentials = WebSocketCredentials(
+ scheme = "Bearer",
+ credentials = getTokenSafely()
+ ),
+ data = json.encodeToJsonElement(DmTypingData(recipientId = recipientId))
+ )
+ )
+ }
+ }
+
+ suspend fun sendStopDmTyping(recipientId: Int) {
+ runCatching {
+ WebSocketManager.send(
+ WebSocketMessage(
+ type = "stopDmTyping",
+ credentials = WebSocketCredentials(
+ scheme = "Bearer",
+ credentials = getTokenSafely()
+ ),
+ data = json.encodeToJsonElement(DmTypingData(recipientId = recipientId))
+ )
+ )
+ }
+ }
+
+ suspend fun sendSubscribeStatus(userId: Int) {
+ runCatching {
+ WebSocketManager.send(
+ WebSocketMessage(
+ type = "subscribeStatus",
+ credentials = WebSocketCredentials(
+ scheme = "Bearer",
+ credentials = getTokenSafely()
+ ),
+ data = json.encodeToJsonElement(SubscribeStatusData(userId = userId))
+ )
+ )
+ }
+ }
+
+ suspend fun sendUnsubscribeStatus(userId: Int) {
+ runCatching {
+ WebSocketManager.send(
+ WebSocketMessage(
+ type = "unsubscribeStatus",
+ credentials = WebSocketCredentials(
+ scheme = "Bearer",
+ credentials = getTokenSafely()
+ ),
+ data = json.encodeToJsonElement(SubscribeStatusData(userId = userId))
+ )
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/Models.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/Models.kt
index 30655d2..42ac89a 100644
--- a/app/shared/src/commonMain/kotlin/ru/fromchat/api/Models.kt
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/Models.kt
@@ -269,6 +269,16 @@ data class WebSocketDeleteMessageRequest(
val message_id: Int
)
+@Serializable
+data class DmTypingData(
+ @SerialName("recipientId") val recipientId: Int
+)
+
+@Serializable
+data class SubscribeStatusData(
+ @SerialName("userId") val userId: Int
+)
+
@Serializable
data class BackupBlobResponse(
val blob: String?
@@ -277,4 +287,15 @@ data class BackupBlobResponse(
@Serializable
data class BackupBlobRequest(
val blob: String
+)
+
+@Serializable
+data class SimilarityResult(
+ @SerialName("isSimilar") val isSimilar: Boolean,
+ @SerialName("similarTo") val similarTo: String? = null
+)
+
+@Serializable
+data class VerifyResponse(
+ val verified: Boolean
)
\ No newline at end of file
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/ProfileCache.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ProfileCache.kt
new file mode 100644
index 0000000..60109d5
--- /dev/null
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ProfileCache.kt
@@ -0,0 +1,15 @@
+package ru.fromchat.api
+
+/**
+ * In-memory cache for user profiles. Used to show profile immediately from cache
+ * and reload in the background.
+ */
+object ProfileCache {
+ private val cache = mutableMapOf()
+
+ fun get(userId: Int): UserProfile? = cache[userId]
+
+ fun put(profile: UserProfile) {
+ cache[profile.id] = profile
+ }
+}
diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/UserStatusStore.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/UserStatusStore.kt
new file mode 100644
index 0000000..f35b89e
--- /dev/null
+++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/UserStatusStore.kt
@@ -0,0 +1,20 @@
+package ru.fromchat.api
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+data class UserStatus(
+ val online: Boolean,
+ val lastSeen: String? = null
+)
+
+object UserStatusStore {
+ private val _status = MutableStateFlow