feat: enhance forwarded messages display by enabling link support
This commit is contained in:
@@ -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)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user