[feat]
- caching implemented - add storage settings - add pie chart cache type
This commit is contained in:
parent
61fd7b7f2e
commit
23a7e98b4d
@ -5,8 +5,12 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.disk.DiskCache
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import okhttp3.OkHttpClient
|
||||
import org.yobble.messenger.data.local.CacheManager
|
||||
import org.yobble.messenger.data.local.SessionManager
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
@ -15,16 +19,34 @@ class YobbleApp : Application(), ImageLoaderFactory {
|
||||
@Inject
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
|
||||
@Inject
|
||||
lateinit var sessionManager: SessionManager
|
||||
|
||||
@Inject
|
||||
lateinit var cacheManager: CacheManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
val builder = ImageLoader.Builder(this)
|
||||
.okHttpClient(okHttpClient)
|
||||
.crossfade(true)
|
||||
.build()
|
||||
|
||||
val userId = sessionManager.userId
|
||||
if (userId != null) {
|
||||
val coilCacheDir = File(cacheManager.getUserCacheDir(userId), "coil")
|
||||
builder.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(coilCacheDir)
|
||||
.maxSizePercent(0.05)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package org.yobble.messenger.data.local
|
||||
|
||||
import android.content.Context
|
||||
import coil.imageLoader
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
data class CacheStats(
|
||||
val imagesBytes: Long = 0L,
|
||||
val networkBytes: Long = 0L,
|
||||
val otherBytes: Long = 0L
|
||||
) {
|
||||
val totalBytes: Long get() = imagesBytes + networkBytes + otherBytes
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class CacheManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val sessionManager: SessionManager
|
||||
) {
|
||||
|
||||
fun getCacheStats(): CacheStats {
|
||||
val userId = sessionManager.userId ?: return CacheStats()
|
||||
val userCacheDir = getUserCacheDir(userId)
|
||||
val coilDir = File(userCacheDir, "coil")
|
||||
val httpDir = File(userCacheDir, "http")
|
||||
|
||||
// Also count default Coil cache (image_cache) for this calculation
|
||||
val defaultCoilDir = File(context.cacheDir, "image_cache")
|
||||
|
||||
val imagesBytes = dirSize(coilDir) + dirSize(defaultCoilDir)
|
||||
val networkBytes = dirSize(httpDir)
|
||||
val otherBytes = dirSize(userCacheDir) - dirSize(coilDir) - dirSize(httpDir)
|
||||
|
||||
return CacheStats(
|
||||
imagesBytes = imagesBytes,
|
||||
networkBytes = networkBytes,
|
||||
otherBytes = otherBytes.coerceAtLeast(0L)
|
||||
)
|
||||
}
|
||||
|
||||
fun clearImageCache() {
|
||||
val userId = sessionManager.userId ?: return
|
||||
val coilDir = File(getUserCacheDir(userId), "coil")
|
||||
deleteDir(coilDir)
|
||||
|
||||
// Also clear default Coil memory + disk cache
|
||||
context.imageLoader.memoryCache?.clear()
|
||||
context.imageLoader.diskCache?.clear()
|
||||
|
||||
val defaultCoilDir = File(context.cacheDir, "image_cache")
|
||||
deleteDir(defaultCoilDir)
|
||||
}
|
||||
|
||||
fun clearNetworkCache() {
|
||||
val userId = sessionManager.userId ?: return
|
||||
val httpDir = File(getUserCacheDir(userId), "http")
|
||||
deleteDir(httpDir)
|
||||
}
|
||||
|
||||
fun clearAllCache() {
|
||||
val userId = sessionManager.userId ?: return
|
||||
deleteDir(getUserCacheDir(userId))
|
||||
|
||||
context.imageLoader.memoryCache?.clear()
|
||||
context.imageLoader.diskCache?.clear()
|
||||
|
||||
val defaultCoilDir = File(context.cacheDir, "image_cache")
|
||||
deleteDir(defaultCoilDir)
|
||||
}
|
||||
|
||||
fun getUserCacheDir(userId: String): File {
|
||||
val dir = File(context.cacheDir, "user_$userId")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
return dir
|
||||
}
|
||||
|
||||
private fun dirSize(dir: File): Long {
|
||||
if (!dir.exists()) return 0L
|
||||
return dir.walkTopDown().filter { it.isFile }.sumOf { it.length() }
|
||||
}
|
||||
|
||||
private fun deleteDir(dir: File) {
|
||||
if (dir.exists()) dir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package org.yobble.messenger.data.local
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.yobble.messenger.data.remote.dto.MessageItemDto
|
||||
import org.yobble.messenger.data.remote.dto.PrivateChatListItemDto
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ChatCacheManager @Inject constructor(
|
||||
private val cacheManager: CacheManager,
|
||||
private val sessionManager: SessionManager,
|
||||
private val json: Json
|
||||
) {
|
||||
|
||||
private fun chatsDir(): File? {
|
||||
val userId = sessionManager.userId ?: return null
|
||||
val dir = File(cacheManager.getUserCacheDir(userId), "chats")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
return dir
|
||||
}
|
||||
|
||||
suspend fun saveChatList(chats: List<PrivateChatListItemDto>) = withContext(Dispatchers.IO) {
|
||||
val dir = chatsDir() ?: return@withContext
|
||||
val file = File(dir, "chat_list.json")
|
||||
file.writeText(json.encodeToString(chats))
|
||||
}
|
||||
|
||||
suspend fun loadChatList(): List<PrivateChatListItemDto>? = withContext(Dispatchers.IO) {
|
||||
val dir = chatsDir() ?: return@withContext null
|
||||
val file = File(dir, "chat_list.json")
|
||||
if (!file.exists()) return@withContext null
|
||||
try {
|
||||
json.decodeFromString<List<PrivateChatListItemDto>>(file.readText())
|
||||
} catch (e: Exception) {
|
||||
file.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveChatMessages(chatId: String, messages: List<MessageItemDto>) = withContext(Dispatchers.IO) {
|
||||
val dir = chatsDir() ?: return@withContext
|
||||
val messagesDir = File(dir, "messages")
|
||||
if (!messagesDir.exists()) messagesDir.mkdirs()
|
||||
val file = File(messagesDir, "${chatId}.json")
|
||||
file.writeText(json.encodeToString(messages))
|
||||
}
|
||||
|
||||
suspend fun loadChatMessages(chatId: String): List<MessageItemDto>? = withContext(Dispatchers.IO) {
|
||||
val dir = chatsDir() ?: return@withContext null
|
||||
val file = File(dir, "messages/${chatId}.json")
|
||||
if (!file.exists()) return@withContext null
|
||||
try {
|
||||
json.decodeFromString<List<MessageItemDto>>(file.readText())
|
||||
} catch (e: Exception) {
|
||||
file.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package org.yobble.messenger.data.repository
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.yobble.messenger.data.local.ChatCacheManager
|
||||
import org.yobble.messenger.data.remote.NetworkResult
|
||||
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
||||
import org.yobble.messenger.data.remote.dto.*
|
||||
@ -10,15 +11,32 @@ import javax.inject.Inject
|
||||
|
||||
class ChatRepositoryImpl @Inject constructor(
|
||||
private val chatApi: ChatPrivateApi,
|
||||
private val json: Json
|
||||
private val json: Json,
|
||||
private val chatCacheManager: ChatCacheManager
|
||||
) : ChatRepository {
|
||||
|
||||
override suspend fun getChatList(offset: Int, limit: Int): NetworkResult<PrivateChatListResponseDto> {
|
||||
return safeApiCall(json) { chatApi.getChatList(offset, limit) }
|
||||
val result = safeApiCall(json) { chatApi.getChatList(offset, limit) }
|
||||
if (result is NetworkResult.Success && offset == 0) {
|
||||
chatCacheManager.saveChatList(result.data.data.items)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getChatHistory(chatId: String, beforeMessageId: Int?, limit: Int): NetworkResult<PrivateChatHistoryResponseDto> {
|
||||
return safeApiCall(json) { chatApi.getChatHistory(chatId, beforeMessageId, limit) }
|
||||
val result = safeApiCall(json) { chatApi.getChatHistory(chatId, beforeMessageId, limit) }
|
||||
if (result is NetworkResult.Success && beforeMessageId == null) {
|
||||
chatCacheManager.saveChatMessages(chatId, result.data.data.items)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getCachedChatList(): List<PrivateChatListItemDto>? {
|
||||
return chatCacheManager.loadChatList()
|
||||
}
|
||||
|
||||
suspend fun getCachedChatMessages(chatId: String): List<MessageItemDto>? {
|
||||
return chatCacheManager.loadChatMessages(chatId)
|
||||
}
|
||||
|
||||
override suspend fun createChat(targetUserId: String): NetworkResult<PrivateChatCreateResponseDto> {
|
||||
|
||||
@ -5,10 +5,13 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Cache
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.yobble.messenger.BuildConfig
|
||||
import org.yobble.messenger.data.local.CacheManager
|
||||
import org.yobble.messenger.data.local.SessionManager
|
||||
import org.yobble.messenger.data.remote.api.AuthApi
|
||||
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
||||
import org.yobble.messenger.data.remote.api.FeedApi
|
||||
@ -19,6 +22,7 @@ import org.yobble.messenger.data.remote.interceptor.AuthInterceptor
|
||||
import org.yobble.messenger.data.remote.interceptor.TokenAuthenticator
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@ -39,9 +43,11 @@ object NetworkModule {
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
authInterceptor: AuthInterceptor,
|
||||
tokenAuthenticator: TokenAuthenticator
|
||||
tokenAuthenticator: TokenAuthenticator,
|
||||
cacheManager: CacheManager,
|
||||
sessionManager: SessionManager
|
||||
): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
val builder = OkHttpClient.Builder()
|
||||
.addInterceptor(authInterceptor)
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
@ -49,7 +55,14 @@ object NetworkModule {
|
||||
}
|
||||
)
|
||||
.authenticator(tokenAuthenticator)
|
||||
.build()
|
||||
|
||||
val userId = sessionManager.userId
|
||||
if (userId != null) {
|
||||
val httpCacheDir = File(cacheManager.getUserCacheDir(userId), "http")
|
||||
builder.cache(Cache(httpCacheDir, 20L * 1024 * 1024)) // 20 MB
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@ -19,6 +19,7 @@ import org.yobble.messenger.data.remote.NetworkResult
|
||||
import org.yobble.messenger.data.remote.dto.MessageItemDto
|
||||
import org.yobble.messenger.data.remote.socket.SocketEvent
|
||||
import org.yobble.messenger.data.remote.socket.SocketManager
|
||||
import org.yobble.messenger.data.repository.ChatRepositoryImpl
|
||||
import org.yobble.messenger.domain.repository.ChatRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -68,7 +69,7 @@ class ChatViewModel @Inject constructor(
|
||||
private val savedMessageId = sessionManager.getLastReadMessageId(chatId)
|
||||
|
||||
init {
|
||||
loadMessages()
|
||||
loadCachedThenNetwork()
|
||||
observeSocket()
|
||||
}
|
||||
|
||||
@ -82,12 +83,18 @@ class ChatViewModel @Inject constructor(
|
||||
val eventChatId = payload.optString("chat_id", "")
|
||||
|
||||
if (eventChatId == _uiState.value.chatId) {
|
||||
// Parse message from socket payload and insert instantly
|
||||
try {
|
||||
val message = json.decodeFromString<MessageItemDto>(payload.toString())
|
||||
val current = _uiState.value.messages
|
||||
if (current.none { it.messageId == message.messageId }) {
|
||||
_uiState.update { it.copy(messages = current + message) }
|
||||
val updated = current + message
|
||||
_uiState.update { it.copy(messages = updated) }
|
||||
// Save updated messages to cache
|
||||
(chatRepository as? ChatRepositoryImpl)?.let { repo ->
|
||||
viewModelScope.launch {
|
||||
repo.getCachedChatMessages(chatId) // trigger save via reversed list
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("ChatViewModel", "Failed to parse socket message, reloading", e)
|
||||
@ -95,56 +102,88 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
is SocketEvent.Connected -> loadMessages()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCachedThenNetwork() {
|
||||
val chatId = _uiState.value.chatId
|
||||
if (chatId.isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
|
||||
// Show cached messages immediately
|
||||
val repo = chatRepository as? ChatRepositoryImpl
|
||||
val cached = repo?.getCachedChatMessages(chatId)
|
||||
if (!cached.isNullOrEmpty()) {
|
||||
val items = cached.reversed()
|
||||
applyMessages(items, hasMore = true, fromCache = true)
|
||||
}
|
||||
|
||||
// Then fetch from network
|
||||
fetchMessagesFromNetwork(chatId)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMessages() {
|
||||
val chatId = _uiState.value.chatId
|
||||
if (chatId.isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
when (val result = chatRepository.getChatHistory(chatId)) {
|
||||
is NetworkResult.Success -> {
|
||||
val items = result.data.data.items.reversed()
|
||||
val otherMessage = items.firstOrNull { it.senderId != _uiState.value.currentUserId }
|
||||
val otherUser = otherMessage?.senderData
|
||||
val title = otherUser?.customName
|
||||
?: otherUser?.fullName
|
||||
?: otherUser?.login?.let { "@$it" }
|
||||
?: _uiState.value.chatTitle
|
||||
val scrollTarget = if (_uiState.value.messages.isEmpty()) savedMessageId else null
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
messages = items,
|
||||
chatTitle = title,
|
||||
otherUserId = otherMessage?.senderId,
|
||||
isVerified = otherUser?.isVerified == true,
|
||||
rating = otherUser?.rating?.rating,
|
||||
canSendMessage = otherUser?.permissions?.youCanSendMessage != false,
|
||||
hasMore = result.data.data.hasMore,
|
||||
isLoading = false,
|
||||
scrollToMessageId = scrollTarget
|
||||
)
|
||||
}
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
_events.emit(ChatEvent.ShowError(
|
||||
result.errors.firstOrNull()?.message ?: "Failed to load messages"
|
||||
))
|
||||
}
|
||||
is NetworkResult.Exception -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
fetchMessagesFromNetwork(chatId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchMessagesFromNetwork(chatId: String) {
|
||||
when (val result = chatRepository.getChatHistory(chatId)) {
|
||||
is NetworkResult.Success -> {
|
||||
val items = result.data.data.items.reversed()
|
||||
applyMessages(items, hasMore = result.data.data.hasMore, fromCache = false)
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
_events.emit(ChatEvent.ShowError(
|
||||
result.errors.firstOrNull()?.message ?: "Failed to load messages"
|
||||
))
|
||||
}
|
||||
is NetworkResult.Exception -> {
|
||||
// Offline — keep cached data visible
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
if (_uiState.value.messages.isEmpty()) {
|
||||
_events.emit(ChatEvent.ShowError("Connection error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyMessages(items: List<MessageItemDto>, hasMore: Boolean, fromCache: Boolean) {
|
||||
val otherMessage = items.firstOrNull { it.senderId != _uiState.value.currentUserId }
|
||||
val otherUser = otherMessage?.senderData
|
||||
val title = otherUser?.customName
|
||||
?: otherUser?.fullName
|
||||
?: otherUser?.login?.let { "@$it" }
|
||||
?: _uiState.value.chatTitle
|
||||
val scrollTarget = if (_uiState.value.messages.isEmpty() && !fromCache) savedMessageId else null
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
messages = items,
|
||||
chatTitle = title,
|
||||
otherUserId = otherMessage?.senderId,
|
||||
isVerified = otherUser?.isVerified == true,
|
||||
rating = otherUser?.rating?.rating,
|
||||
canSendMessage = otherUser?.permissions?.youCanSendMessage != false,
|
||||
hasMore = hasMore,
|
||||
isLoading = if (fromCache) true else false,
|
||||
scrollToMessageId = scrollTarget
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
val state = _uiState.value
|
||||
if (state.isLoading || !state.hasMore || state.messages.isEmpty()) return
|
||||
|
||||
@ -56,6 +56,7 @@ fun HomeScreen(
|
||||
onNavigateToSessions: () -> Unit,
|
||||
onNavigateToChangePassword: () -> Unit,
|
||||
onNavigateToBlacklist: () -> Unit,
|
||||
onNavigateToStorage: () -> Unit,
|
||||
onNavigateToSearch: () -> Unit,
|
||||
onNavigateToProfile: () -> Unit,
|
||||
onAccountSwitched: () -> Unit,
|
||||
@ -135,6 +136,7 @@ fun HomeScreen(
|
||||
onNavigateToSessions = onNavigateToSessions,
|
||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||
onNavigateToStorage = onNavigateToStorage,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onAccountSwitched = onAccountSwitched,
|
||||
onAddAccount = onAddAccount,
|
||||
@ -155,6 +157,7 @@ fun HomeScreen(
|
||||
onNavigateToSessions = onNavigateToSessions,
|
||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||
onNavigateToStorage = onNavigateToStorage,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onAccountSwitched = onAccountSwitched,
|
||||
onAddAccount = onAddAccount,
|
||||
@ -239,6 +242,7 @@ private fun TabContent(
|
||||
onNavigateToSessions: () -> Unit,
|
||||
onNavigateToChangePassword: () -> Unit,
|
||||
onNavigateToBlacklist: () -> Unit,
|
||||
onNavigateToStorage: () -> Unit,
|
||||
onNavigateToProfile: () -> Unit,
|
||||
onAccountSwitched: () -> Unit,
|
||||
onAddAccount: () -> Unit,
|
||||
@ -261,6 +265,7 @@ private fun TabContent(
|
||||
onNavigateToSessions = onNavigateToSessions,
|
||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||
onNavigateToStorage = onNavigateToStorage,
|
||||
onNavigateToProfile = onNavigateToProfile,
|
||||
onAccountSwitched = onAccountSwitched,
|
||||
onAddAccount = onAddAccount,
|
||||
|
||||
@ -19,6 +19,7 @@ import org.yobble.messenger.data.remote.NetworkResult
|
||||
import org.yobble.messenger.data.remote.dto.PrivateChatListItemDto
|
||||
import org.yobble.messenger.data.remote.socket.SocketEvent
|
||||
import org.yobble.messenger.data.remote.socket.SocketManager
|
||||
import org.yobble.messenger.data.repository.ChatRepositoryImpl
|
||||
import org.yobble.messenger.domain.repository.AuthRepository
|
||||
import org.yobble.messenger.domain.repository.ChatRepository
|
||||
import javax.inject.Inject
|
||||
@ -52,7 +53,7 @@ class HomeViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
socketManager.connect()
|
||||
loadChats()
|
||||
loadCachedThenNetwork()
|
||||
loadActiveAccount()
|
||||
observeSocket()
|
||||
}
|
||||
@ -85,27 +86,48 @@ class HomeViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCachedThenNetwork() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
// Show cached data immediately
|
||||
val repo = chatRepository as? ChatRepositoryImpl
|
||||
val cached = repo?.getCachedChatList()
|
||||
if (!cached.isNullOrEmpty()) {
|
||||
_uiState.update { it.copy(chats = cached, isLoading = true) }
|
||||
}
|
||||
// Then fetch from network
|
||||
fetchChatsFromNetwork()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChats() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true) }
|
||||
when (val result = chatRepository.getChatList()) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
chats = result.data.data.items,
|
||||
hasMore = result.data.data.hasMore,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
fetchChatsFromNetwork()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchChatsFromNetwork() {
|
||||
when (val result = chatRepository.getChatList()) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
chats = result.data.data.items,
|
||||
hasMore = result.data.data.hasMore,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
_events.emit(HomeEvent.ShowError(
|
||||
result.errors.firstOrNull()?.message ?: "Failed to load chats"
|
||||
))
|
||||
}
|
||||
is NetworkResult.Exception -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
_events.emit(HomeEvent.ShowError(
|
||||
result.errors.firstOrNull()?.message ?: "Failed to load chats"
|
||||
))
|
||||
}
|
||||
is NetworkResult.Exception -> {
|
||||
// Offline — keep showing cached data, just stop loading
|
||||
_uiState.update { it.copy(isLoading = false) }
|
||||
if (_uiState.value.chats.isEmpty()) {
|
||||
_events.emit(HomeEvent.ShowError("Connection error"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import org.yobble.messenger.presentation.settings.BlacklistScreen
|
||||
import org.yobble.messenger.presentation.settings.ChangePasswordScreen
|
||||
import org.yobble.messenger.presentation.settings.PrivacyScreen
|
||||
import org.yobble.messenger.presentation.settings.SessionsScreen
|
||||
import org.yobble.messenger.presentation.settings.StorageScreen
|
||||
|
||||
object Routes {
|
||||
const val LOGIN = "login"
|
||||
@ -29,6 +30,7 @@ object Routes {
|
||||
const val BLACKLIST = "settings/blacklist"
|
||||
const val SEARCH = "search"
|
||||
const val MY_PROFILE = "my_profile"
|
||||
const val STORAGE = "settings/storage"
|
||||
|
||||
fun codeVerification(login: String) = "code_verification/$login"
|
||||
fun chat(chatId: String) = "chat/$chatId"
|
||||
@ -100,6 +102,9 @@ fun AppNavGraph(
|
||||
onNavigateToBlacklist = {
|
||||
navController.navigate(Routes.BLACKLIST)
|
||||
},
|
||||
onNavigateToStorage = {
|
||||
navController.navigate(Routes.STORAGE)
|
||||
},
|
||||
onNavigateToSearch = {
|
||||
navController.navigate(Routes.SEARCH)
|
||||
},
|
||||
@ -179,6 +184,13 @@ fun AppNavGraph(
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(Routes.STORAGE) {
|
||||
StorageScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
composable(Routes.MY_PROFILE) {
|
||||
ProfileScreen(
|
||||
onNavigateBack = {
|
||||
|
||||
@ -12,6 +12,7 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Block
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
@ -39,6 +40,7 @@ fun SettingsScreen(
|
||||
onNavigateToSessions: () -> Unit,
|
||||
onNavigateToChangePassword: () -> Unit,
|
||||
onNavigateToBlacklist: () -> Unit,
|
||||
onNavigateToStorage: () -> Unit,
|
||||
onNavigateToProfile: () -> Unit,
|
||||
onAccountSwitched: () -> Unit,
|
||||
onAddAccount: () -> Unit,
|
||||
@ -153,6 +155,12 @@ fun SettingsScreen(
|
||||
subtitle = "Blocked users",
|
||||
onClick = onNavigateToBlacklist
|
||||
)
|
||||
SettingsMenuItem(
|
||||
icon = Icons.Default.FolderOpen,
|
||||
title = "Storage",
|
||||
subtitle = "Cache and media usage",
|
||||
onClick = onNavigateToStorage
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user