diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt index 93859d1..7a354d3 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt @@ -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, + 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) { + 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>(emptyList()) } - val memberInfoByKey = remember(dialogPublicKey) { mutableStateMapOf() } - // Real online status from PacketOnlineState (0x05), NOT from SearchUser.online - val memberOnlineStatus = remember(dialogPublicKey) { mutableStateMapOf() } 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().apply { + initialMemoryMembersCache?.memberInfoByKey?.let { putAll(it) } + } + } + // Real online status from PacketOnlineState (0x05), NOT from SearchUser.online + val memberOnlineStatus = remember(dialogPublicKey) { mutableStateMapOf() } val groupEntity by produceState( 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() + 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 {