Добавлен кэширование участников групп в памяти и на диске
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user