Добавлен кэширование участников групп в памяти и на диске
This commit is contained in:
@@ -149,6 +149,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
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(
|
private data class GroupMediaItem(
|
||||||
val key: String,
|
val key: String,
|
||||||
val attachment: MessageAttachment,
|
val attachment: MessageAttachment,
|
||||||
@@ -335,13 +405,27 @@ fun GroupInfoScreen(
|
|||||||
var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
|
var pendingInviteText by rememberSaveable(dialogPublicKey) { mutableStateOf("") }
|
||||||
var isRefreshingMembers by remember(dialogPublicKey) { mutableStateOf(false) }
|
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) {
|
val membersCacheKey = remember(currentUserPublicKey, normalizedGroupId) {
|
||||||
"${currentUserPublicKey.trim().lowercase()}|${normalizedGroupId.trim().lowercase()}"
|
"${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?>(
|
val groupEntity by produceState<com.rosetta.messenger.database.GroupEntity?>(
|
||||||
initialValue = null,
|
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
|
val shouldShowLoader = showLoader && members.isEmpty() && !hasAnyCache
|
||||||
if (shouldShowLoader) membersLoading = true
|
if (shouldShowLoader) membersLoading = true
|
||||||
isRefreshingMembers = true
|
isRefreshingMembers = true
|
||||||
@@ -528,6 +614,7 @@ fun GroupInfoScreen(
|
|||||||
members = members,
|
members = members,
|
||||||
memberInfoByKey = memberInfoByKey.toMap()
|
memberInfoByKey = memberInfoByKey.toMap()
|
||||||
)
|
)
|
||||||
|
GroupMembersDiskCache.put(context, membersCacheKey, members)
|
||||||
} finally {
|
} finally {
|
||||||
if (shouldShowLoader) membersLoading = false
|
if (shouldShowLoader) membersLoading = false
|
||||||
isRefreshingMembers = false
|
isRefreshingMembers = false
|
||||||
@@ -536,15 +623,44 @@ fun GroupInfoScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(membersCacheKey) {
|
LaunchedEffect(membersCacheKey) {
|
||||||
val cachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
|
val memoryCachedEntry = GroupMembersMemoryCache.getAny(membersCacheKey)
|
||||||
cachedEntry?.let { cached ->
|
val diskCachedEntry = GroupMembersDiskCache.getAny(context, membersCacheKey)
|
||||||
|
|
||||||
|
memoryCachedEntry?.let { cached ->
|
||||||
members = cached.members
|
members = cached.members
|
||||||
memberInfoByKey.clear()
|
memberInfoByKey.clear()
|
||||||
memberInfoByKey.putAll(cached.memberInfoByKey)
|
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) {
|
val hasFreshCache =
|
||||||
refreshMembers(force = true, showLoader = cachedEntry == null)
|
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
|
isLeaving = false
|
||||||
if (left) {
|
if (left) {
|
||||||
GroupMembersMemoryCache.remove(membersCacheKey)
|
GroupMembersMemoryCache.remove(membersCacheKey)
|
||||||
|
GroupMembersDiskCache.remove(context, membersCacheKey)
|
||||||
onGroupLeft()
|
onGroupLeft()
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Failed to leave group", Toast.LENGTH_SHORT).show()
|
||||||
@@ -831,6 +948,7 @@ fun GroupInfoScreen(
|
|||||||
members = members,
|
members = members,
|
||||||
memberInfoByKey = memberInfoByKey.toMap()
|
memberInfoByKey = memberInfoByKey.toMap()
|
||||||
)
|
)
|
||||||
|
GroupMembersDiskCache.put(context, membersCacheKey, members)
|
||||||
refreshMembers(force = true, showLoader = false)
|
refreshMembers(force = true, showLoader = false)
|
||||||
Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Member removed", Toast.LENGTH_SHORT).show()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user