feat: enhance forwarded messages display by enabling link support

This commit is contained in:
2026-02-12 08:15:11 +05:00
parent 6f195f4d09
commit f7ece6055e
7 changed files with 564 additions and 214 deletions

View File

@@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.resume
/**
* Singleton manager for Protocol instance
@@ -34,6 +36,12 @@ object ProtocolManager {
private val _typingUsers = MutableStateFlow<Set<String>>(emptySet())
val typingUsers: StateFlow<Set<String>> = _typingUsers.asStateFlow()
// 🔍 Global user info cache (like Desktop's InformationProvider.cachedUsers)
// publicKey → SearchUser (resolved via PacketSearch 0x03)
private val userInfoCache = ConcurrentHashMap<String, SearchUser>()
// Pending resolves: publicKey → list of continuations waiting for the result
private val pendingResolves = ConcurrentHashMap<String, MutableList<kotlinx.coroutines.CancellableContinuation<SearchUser?>>>()
private val dateFormat = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())
// 🚀 Флаг для включения UI логов (по умолчанию ВЫКЛЮЧЕНО - это вызывало ANR!)
@@ -167,6 +175,14 @@ object ProtocolManager {
scope.launch(Dispatchers.IO) {
val ownPublicKey = getProtocol().getPublicKey()
searchPacket.users.forEach { user ->
// 🔍 Кэшируем всех пользователей (desktop parity: cachedUsers)
userInfoCache[user.publicKey] = user
// Resume pending resolves for this publicKey
pendingResolves.remove(user.publicKey)?.forEach { cont ->
try { cont.resume(user) } catch (_: Exception) {}
}
// Обновляем инфо в диалогах (для всех пользователей)
messageRepository?.updateDialogUserInfo(
user.publicKey,
@@ -188,6 +204,13 @@ object ProtocolManager {
}
}
}
// Resume pending resolves that got empty response (no match)
if (searchPacket.search.isNotEmpty() && searchPacket.users.none { it.publicKey == searchPacket.search }) {
pendingResolves.remove(searchPacket.search)?.forEach { cont ->
try { cont.resume(null) } catch (_: Exception) {}
}
}
}
// 🚀 Обработчик транспортного сервера (0x0F)
@@ -255,6 +278,104 @@ object ProtocolManager {
send(packet)
}
/**
* 🔍 Resolve publicKey → user title (like Desktop useUserInformation)
* Checks cache first, then sends PacketSearch and waits for response.
* Returns title or null on timeout/not found.
* @param timeoutMs max wait time for server response (default 3s)
*/
suspend fun resolveUserName(publicKey: String, timeoutMs: Long = 3000): String? {
if (publicKey.isEmpty()) return null
// 1. Check in-memory cache (instant)
userInfoCache[publicKey]?.let { cached ->
val name = cached.title.ifEmpty { cached.username }
if (name.isNotEmpty()) return name
}
// 2. Send PacketSearch and wait for response via suspendCancellableCoroutine
val privateHash = try { getProtocol().getPrivateHash() } catch (_: Exception) { null }
?: return null
return try {
withTimeout(timeoutMs) {
suspendCancellableCoroutine { cont ->
// Register continuation in pending list
pendingResolves.getOrPut(publicKey) { mutableListOf() }.add(cont)
cont.invokeOnCancellation {
pendingResolves[publicKey]?.remove(cont)
if (pendingResolves[publicKey]?.isEmpty() == true) {
pendingResolves.remove(publicKey)
}
}
// Send search request
val packet = PacketSearch().apply {
this.privateKey = privateHash
this.search = publicKey
}
send(packet)
}
}?.let { user -> user.title.ifEmpty { user.username }.ifEmpty { null } }
} catch (_: Exception) {
// Timeout or cancellation — clean up
pendingResolves.remove(publicKey)
null
}
}
/**
* 🔍 Get cached user info (no network request)
*/
fun getCachedUserName(publicKey: String): String? {
val cached = userInfoCache[publicKey] ?: return null
return cached.title.ifEmpty { cached.username }.ifEmpty { null }
}
/**
* 🔍 Get full cached user info (no network request)
*/
fun getCachedUserInfo(publicKey: String): SearchUser? {
return userInfoCache[publicKey]
}
/**
* 🔍 Resolve publicKey → full SearchUser (with server request if needed)
*/
suspend fun resolveUserInfo(publicKey: String, timeoutMs: Long = 3000): SearchUser? {
if (publicKey.isEmpty()) return null
// 1. Check in-memory cache
userInfoCache[publicKey]?.let { return it }
// 2. Send PacketSearch and wait
val privateHash = try { getProtocol().getPrivateHash() } catch (_: Exception) { null }
?: return null
return try {
withTimeout(timeoutMs) {
suspendCancellableCoroutine { cont ->
pendingResolves.getOrPut(publicKey) { mutableListOf() }.add(cont)
cont.invokeOnCancellation {
pendingResolves[publicKey]?.remove(cont)
if (pendingResolves[publicKey]?.isEmpty() == true) {
pendingResolves.remove(publicKey)
}
}
val packet = PacketSearch().apply {
this.privateKey = privateHash
this.search = publicKey
}
send(packet)
}
}
} catch (_: Exception) {
pendingResolves.remove(publicKey)
null
}
}
/**
* Send packet (simplified)
*/