Добавлен кэширование участников групп в памяти и на диске

This commit is contained in:
2026-03-09 19:23:28 +05:00
parent 1b3c4c8cea
commit 41faa98130

View File

@@ -149,6 +149,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.core.view.WindowCompat
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -214,6 +215,75 @@ private object GroupMembersMemoryCache {
}
}
private data class GroupMembersDiskCacheEntry(
val members: List<String>,
val updatedAtMs: Long
)
private object GroupMembersDiskCache {
private const val PREFS_NAME = "group_members_cache"
private const val KEY_PREFIX = "entry_"
private const val TTL_MS = 12 * 60 * 60 * 1000L
fun getAny(context: Context, key: String): GroupMembersDiskCacheEntry? = read(context, key)
fun getFresh(context: Context, key: String): GroupMembersDiskCacheEntry? {
val entry = read(context, key) ?: return null
return if (System.currentTimeMillis() - entry.updatedAtMs <= TTL_MS) entry else null
}
fun put(context: Context, key: String, members: List<String>) {
if (key.isBlank()) return
val normalizedMembers = members.map { it.trim() }.filter { it.isNotBlank() }.distinct()
if (normalizedMembers.isEmpty()) return
val payload =
JSONObject().apply {
put("updatedAtMs", System.currentTimeMillis())
put("members", JSONArray().apply { normalizedMembers.forEach { put(it) } })
}.toString()
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_PREFIX + key, payload)
.apply()
}
fun remove(context: Context, key: String) {
if (key.isBlank()) return
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.remove(KEY_PREFIX + key)
.apply()
}
private fun read(context: Context, key: String): GroupMembersDiskCacheEntry? {
if (key.isBlank()) return null
val raw =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString(KEY_PREFIX + key, null) ?: return null
return runCatching {
val json = JSONObject(raw)
val membersArray = json.optJSONArray("members") ?: JSONArray()
val membersList =
buildList {
repeat(membersArray.length()) { index ->
val value = membersArray.optString(index).trim()
if (value.isNotBlank()) add(value)
}
}
.distinct()
if (membersList.isEmpty()) {
null
} else {
GroupMembersDiskCacheEntry(
members = membersList,
updatedAtMs = json.optLong("updatedAtMs", 0L)
)
}
}
.getOrNull()
}
}
private data class GroupMediaItem(
val key: String,
val attachment: MessageAttachment,
@@ -335,13 +405,27 @@ fun GroupInfoScreen(
var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
var isRefreshingMembers by remember(dialogPublicKey) { mutableStateOf(false) }
var members by remember(dialogPublicKey) { mutableStateOf<List<String>>(emptyList()) }
val memberInfoByKey = remember(dialogPublicKey) { mutableStateMapOf<String, SearchUser>() }
// Real online status from PacketOnlineState (0x05), NOT from SearchUser.online
val memberOnlineStatus = remember(dialogPublicKey) { mutableStateMapOf<String, Boolean>() }
val membersCacheKey = remember(currentUserPublicKey, normalizedGroupId) {
"${currentUserPublicKey.trim().lowercase()}|${normalizedGroupId.trim().lowercase()}"
}
val initialMemoryMembersCache = remember(membersCacheKey) {
GroupMembersMemoryCache.getAny(membersCacheKey)
}
val initialDiskMembersCache = remember(membersCacheKey) {
GroupMembersDiskCache.getAny(context, membersCacheKey)
}
val initialMembers = remember(initialMemoryMembersCache, initialDiskMembersCache) {
initialMemoryMembersCache?.members ?: initialDiskMembersCache?.members.orEmpty()
}
var members by remember(dialogPublicKey) { mutableStateOf(initialMembers) }
val memberInfoByKey =
remember(dialogPublicKey) {
mutableStateMapOf<String, SearchUser>().apply {
initialMemoryMembersCache?.memberInfoByKey?.let { putAll(it) }
}
}
// Real online status from PacketOnlineState (0x05), NOT from SearchUser.online
val memberOnlineStatus = remember(dialogPublicKey) { mutableStateMapOf<String, Boolean>() }
val groupEntity by produceState<com.rosetta.messenger.database.GroupEntity?>(
initialValue = null,
@@ -490,7 +574,9 @@ fun GroupInfoScreen(
}
}
val hasAnyCache = GroupMembersMemoryCache.getAny(membersCacheKey) != null
val hasAnyCache =
GroupMembersMemoryCache.getAny(membersCacheKey) != null ||
GroupMembersDiskCache.getAny(context, membersCacheKey) != null
val shouldShowLoader = showLoader && members.isEmpty() && !hasAnyCache
if (shouldShowLoader) membersLoading = true
isRefreshingMembers = true
@@ -528,6 +614,7 @@ fun GroupInfoScreen(
members = members,
memberInfoByKey = memberInfoByKey.toMap()
)
GroupMembersDiskCache.put(context, membersCacheKey, members)
} finally {
if (shouldShowLoader) membersLoading = false
isRefreshingMembers = false
@@ -536,15 +623,44 @@ fun GroupInfoScreen(
}
LaunchedEffect(membersCacheKey) {
val cachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
cachedEntry?.let { cached ->
val memoryCachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
val diskCachedEntry = GroupMembersDiskCache.getAny(context, membersCacheKey)
memoryCachedEntry?.let { cached ->
members = cached.members
memberInfoByKey.clear()
memberInfoByKey.putAll(cached.memberInfoByKey)
} ?: diskCachedEntry?.let { cached ->
members = cached.members
if (memberInfoByKey.isEmpty()) {
val resolvedUsers = withContext(Dispatchers.IO) {
val resolvedMap = LinkedHashMap<String, SearchUser>()
cached.members.forEach { memberKey ->
ProtocolManager.getCachedUserInfo(memberKey)?.let { resolved ->
resolvedMap[memberKey] = resolved
}
}
resolvedMap
}
if (resolvedUsers.isNotEmpty()) {
memberInfoByKey.putAll(resolvedUsers)
}
}
GroupMembersMemoryCache.put(
key = membersCacheKey,
members = members,
memberInfoByKey = memberInfoByKey.toMap()
)
}
if (GroupMembersMemoryCache.getFresh(membersCacheKey) == null) {
refreshMembers(force = true, showLoader = cachedEntry == null)
val hasFreshCache =
GroupMembersMemoryCache.getFresh(membersCacheKey) != null ||
GroupMembersDiskCache.getFresh(context, membersCacheKey) != null
if (!hasFreshCache) {
refreshMembers(
force = true,
showLoader = memoryCachedEntry == null && diskCachedEntry == null
)
}
}
@@ -787,6 +903,7 @@ fun GroupInfoScreen(
isLeaving = false
if (left) {
GroupMembersMemoryCache.remove(membersCacheKey)
GroupMembersDiskCache.remove(context, membersCacheKey)
onGroupLeft()
} else {
Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
@@ -831,6 +948,7 @@ fun GroupInfoScreen(
members = members,
memberInfoByKey = memberInfoByKey.toMap()
)
GroupMembersDiskCache.put(context, membersCacheKey, members)
refreshMembers(force = true, showLoader = false)
Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
} else {