[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 android.app.NotificationManager
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
|
import coil.disk.DiskCache
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import okhttp3.OkHttpClient
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@ -15,16 +19,34 @@ class YobbleApp : Application(), ImageLoaderFactory {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var okHttpClient: OkHttpClient
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionManager: SessionManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cacheManager: CacheManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
createNotificationChannels()
|
createNotificationChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
return ImageLoader.Builder(this)
|
val builder = ImageLoader.Builder(this)
|
||||||
.okHttpClient(okHttpClient)
|
.okHttpClient(okHttpClient)
|
||||||
.crossfade(true)
|
.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() {
|
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
|
package org.yobble.messenger.data.repository
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
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.NetworkResult
|
||||||
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
||||||
import org.yobble.messenger.data.remote.dto.*
|
import org.yobble.messenger.data.remote.dto.*
|
||||||
@ -10,15 +11,32 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class ChatRepositoryImpl @Inject constructor(
|
class ChatRepositoryImpl @Inject constructor(
|
||||||
private val chatApi: ChatPrivateApi,
|
private val chatApi: ChatPrivateApi,
|
||||||
private val json: Json
|
private val json: Json,
|
||||||
|
private val chatCacheManager: ChatCacheManager
|
||||||
) : ChatRepository {
|
) : ChatRepository {
|
||||||
|
|
||||||
override suspend fun getChatList(offset: Int, limit: Int): NetworkResult<PrivateChatListResponseDto> {
|
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> {
|
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> {
|
override suspend fun createChat(targetUserId: String): NetworkResult<PrivateChatCreateResponseDto> {
|
||||||
|
|||||||
@ -5,10 +5,13 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Cache
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.yobble.messenger.BuildConfig
|
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.AuthApi
|
||||||
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
import org.yobble.messenger.data.remote.api.ChatPrivateApi
|
||||||
import org.yobble.messenger.data.remote.api.FeedApi
|
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 org.yobble.messenger.data.remote.interceptor.TokenAuthenticator
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
|
import java.io.File
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ -39,9 +43,11 @@ object NetworkModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(
|
fun provideOkHttpClient(
|
||||||
authInterceptor: AuthInterceptor,
|
authInterceptor: AuthInterceptor,
|
||||||
tokenAuthenticator: TokenAuthenticator
|
tokenAuthenticator: TokenAuthenticator,
|
||||||
|
cacheManager: CacheManager,
|
||||||
|
sessionManager: SessionManager
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
return OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
.addInterceptor(authInterceptor)
|
.addInterceptor(authInterceptor)
|
||||||
.addInterceptor(
|
.addInterceptor(
|
||||||
HttpLoggingInterceptor().apply {
|
HttpLoggingInterceptor().apply {
|
||||||
@ -49,7 +55,14 @@ object NetworkModule {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.authenticator(tokenAuthenticator)
|
.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
|
@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.dto.MessageItemDto
|
||||||
import org.yobble.messenger.data.remote.socket.SocketEvent
|
import org.yobble.messenger.data.remote.socket.SocketEvent
|
||||||
import org.yobble.messenger.data.remote.socket.SocketManager
|
import org.yobble.messenger.data.remote.socket.SocketManager
|
||||||
|
import org.yobble.messenger.data.repository.ChatRepositoryImpl
|
||||||
import org.yobble.messenger.domain.repository.ChatRepository
|
import org.yobble.messenger.domain.repository.ChatRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -68,7 +69,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
private val savedMessageId = sessionManager.getLastReadMessageId(chatId)
|
private val savedMessageId = sessionManager.getLastReadMessageId(chatId)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadMessages()
|
loadCachedThenNetwork()
|
||||||
observeSocket()
|
observeSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,12 +83,18 @@ class ChatViewModel @Inject constructor(
|
|||||||
val eventChatId = payload.optString("chat_id", "")
|
val eventChatId = payload.optString("chat_id", "")
|
||||||
|
|
||||||
if (eventChatId == _uiState.value.chatId) {
|
if (eventChatId == _uiState.value.chatId) {
|
||||||
// Parse message from socket payload and insert instantly
|
|
||||||
try {
|
try {
|
||||||
val message = json.decodeFromString<MessageItemDto>(payload.toString())
|
val message = json.decodeFromString<MessageItemDto>(payload.toString())
|
||||||
val current = _uiState.value.messages
|
val current = _uiState.value.messages
|
||||||
if (current.none { it.messageId == message.messageId }) {
|
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) {
|
} catch (e: Exception) {
|
||||||
Log.w("ChatViewModel", "Failed to parse socket message, reloading", e)
|
Log.w("ChatViewModel", "Failed to parse socket message, reloading", e)
|
||||||
@ -95,56 +102,88 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is SocketEvent.Connected -> loadMessages()
|
||||||
else -> {}
|
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() {
|
fun loadMessages() {
|
||||||
val chatId = _uiState.value.chatId
|
val chatId = _uiState.value.chatId
|
||||||
if (chatId.isBlank()) return
|
if (chatId.isBlank()) return
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isLoading = true) }
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
when (val result = chatRepository.getChatHistory(chatId)) {
|
fetchMessagesFromNetwork(chatId)
|
||||||
is NetworkResult.Success -> {
|
}
|
||||||
val items = result.data.data.items.reversed()
|
}
|
||||||
val otherMessage = items.firstOrNull { it.senderId != _uiState.value.currentUserId }
|
|
||||||
val otherUser = otherMessage?.senderData
|
private suspend fun fetchMessagesFromNetwork(chatId: String) {
|
||||||
val title = otherUser?.customName
|
when (val result = chatRepository.getChatHistory(chatId)) {
|
||||||
?: otherUser?.fullName
|
is NetworkResult.Success -> {
|
||||||
?: otherUser?.login?.let { "@$it" }
|
val items = result.data.data.items.reversed()
|
||||||
?: _uiState.value.chatTitle
|
applyMessages(items, hasMore = result.data.data.hasMore, fromCache = false)
|
||||||
val scrollTarget = if (_uiState.value.messages.isEmpty()) savedMessageId else null
|
}
|
||||||
_uiState.update {
|
is NetworkResult.Error -> {
|
||||||
it.copy(
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
messages = items,
|
_events.emit(ChatEvent.ShowError(
|
||||||
chatTitle = title,
|
result.errors.firstOrNull()?.message ?: "Failed to load messages"
|
||||||
otherUserId = otherMessage?.senderId,
|
))
|
||||||
isVerified = otherUser?.isVerified == true,
|
}
|
||||||
rating = otherUser?.rating?.rating,
|
is NetworkResult.Exception -> {
|
||||||
canSendMessage = otherUser?.permissions?.youCanSendMessage != false,
|
// Offline — keep cached data visible
|
||||||
hasMore = result.data.data.hasMore,
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
isLoading = false,
|
if (_uiState.value.messages.isEmpty()) {
|
||||||
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) }
|
|
||||||
_events.emit(ChatEvent.ShowError("Connection error"))
|
_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() {
|
fun loadMore() {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
if (state.isLoading || !state.hasMore || state.messages.isEmpty()) return
|
if (state.isLoading || !state.hasMore || state.messages.isEmpty()) return
|
||||||
|
|||||||
@ -56,6 +56,7 @@ fun HomeScreen(
|
|||||||
onNavigateToSessions: () -> Unit,
|
onNavigateToSessions: () -> Unit,
|
||||||
onNavigateToChangePassword: () -> Unit,
|
onNavigateToChangePassword: () -> Unit,
|
||||||
onNavigateToBlacklist: () -> Unit,
|
onNavigateToBlacklist: () -> Unit,
|
||||||
|
onNavigateToStorage: () -> Unit,
|
||||||
onNavigateToSearch: () -> Unit,
|
onNavigateToSearch: () -> Unit,
|
||||||
onNavigateToProfile: () -> Unit,
|
onNavigateToProfile: () -> Unit,
|
||||||
onAccountSwitched: () -> Unit,
|
onAccountSwitched: () -> Unit,
|
||||||
@ -135,6 +136,7 @@ fun HomeScreen(
|
|||||||
onNavigateToSessions = onNavigateToSessions,
|
onNavigateToSessions = onNavigateToSessions,
|
||||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||||
|
onNavigateToStorage = onNavigateToStorage,
|
||||||
onNavigateToProfile = onNavigateToProfile,
|
onNavigateToProfile = onNavigateToProfile,
|
||||||
onAccountSwitched = onAccountSwitched,
|
onAccountSwitched = onAccountSwitched,
|
||||||
onAddAccount = onAddAccount,
|
onAddAccount = onAddAccount,
|
||||||
@ -155,6 +157,7 @@ fun HomeScreen(
|
|||||||
onNavigateToSessions = onNavigateToSessions,
|
onNavigateToSessions = onNavigateToSessions,
|
||||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||||
|
onNavigateToStorage = onNavigateToStorage,
|
||||||
onNavigateToProfile = onNavigateToProfile,
|
onNavigateToProfile = onNavigateToProfile,
|
||||||
onAccountSwitched = onAccountSwitched,
|
onAccountSwitched = onAccountSwitched,
|
||||||
onAddAccount = onAddAccount,
|
onAddAccount = onAddAccount,
|
||||||
@ -239,6 +242,7 @@ private fun TabContent(
|
|||||||
onNavigateToSessions: () -> Unit,
|
onNavigateToSessions: () -> Unit,
|
||||||
onNavigateToChangePassword: () -> Unit,
|
onNavigateToChangePassword: () -> Unit,
|
||||||
onNavigateToBlacklist: () -> Unit,
|
onNavigateToBlacklist: () -> Unit,
|
||||||
|
onNavigateToStorage: () -> Unit,
|
||||||
onNavigateToProfile: () -> Unit,
|
onNavigateToProfile: () -> Unit,
|
||||||
onAccountSwitched: () -> Unit,
|
onAccountSwitched: () -> Unit,
|
||||||
onAddAccount: () -> Unit,
|
onAddAccount: () -> Unit,
|
||||||
@ -261,6 +265,7 @@ private fun TabContent(
|
|||||||
onNavigateToSessions = onNavigateToSessions,
|
onNavigateToSessions = onNavigateToSessions,
|
||||||
onNavigateToChangePassword = onNavigateToChangePassword,
|
onNavigateToChangePassword = onNavigateToChangePassword,
|
||||||
onNavigateToBlacklist = onNavigateToBlacklist,
|
onNavigateToBlacklist = onNavigateToBlacklist,
|
||||||
|
onNavigateToStorage = onNavigateToStorage,
|
||||||
onNavigateToProfile = onNavigateToProfile,
|
onNavigateToProfile = onNavigateToProfile,
|
||||||
onAccountSwitched = onAccountSwitched,
|
onAccountSwitched = onAccountSwitched,
|
||||||
onAddAccount = onAddAccount,
|
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.dto.PrivateChatListItemDto
|
||||||
import org.yobble.messenger.data.remote.socket.SocketEvent
|
import org.yobble.messenger.data.remote.socket.SocketEvent
|
||||||
import org.yobble.messenger.data.remote.socket.SocketManager
|
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.AuthRepository
|
||||||
import org.yobble.messenger.domain.repository.ChatRepository
|
import org.yobble.messenger.domain.repository.ChatRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -52,7 +53,7 @@ class HomeViewModel @Inject constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
socketManager.connect()
|
socketManager.connect()
|
||||||
loadChats()
|
loadCachedThenNetwork()
|
||||||
loadActiveAccount()
|
loadActiveAccount()
|
||||||
observeSocket()
|
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() {
|
fun loadChats() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isLoading = true) }
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
when (val result = chatRepository.getChatList()) {
|
fetchChatsFromNetwork()
|
||||||
is NetworkResult.Success -> {
|
}
|
||||||
_uiState.update {
|
}
|
||||||
it.copy(
|
|
||||||
chats = result.data.data.items,
|
private suspend fun fetchChatsFromNetwork() {
|
||||||
hasMore = result.data.data.hasMore,
|
when (val result = chatRepository.getChatList()) {
|
||||||
isLoading = false
|
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) }
|
is NetworkResult.Error -> {
|
||||||
_events.emit(HomeEvent.ShowError(
|
_uiState.update { it.copy(isLoading = false) }
|
||||||
result.errors.firstOrNull()?.message ?: "Failed to load chats"
|
_events.emit(HomeEvent.ShowError(
|
||||||
))
|
result.errors.firstOrNull()?.message ?: "Failed to load chats"
|
||||||
}
|
))
|
||||||
is NetworkResult.Exception -> {
|
}
|
||||||
_uiState.update { it.copy(isLoading = false) }
|
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"))
|
_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.ChangePasswordScreen
|
||||||
import org.yobble.messenger.presentation.settings.PrivacyScreen
|
import org.yobble.messenger.presentation.settings.PrivacyScreen
|
||||||
import org.yobble.messenger.presentation.settings.SessionsScreen
|
import org.yobble.messenger.presentation.settings.SessionsScreen
|
||||||
|
import org.yobble.messenger.presentation.settings.StorageScreen
|
||||||
|
|
||||||
object Routes {
|
object Routes {
|
||||||
const val LOGIN = "login"
|
const val LOGIN = "login"
|
||||||
@ -29,6 +30,7 @@ object Routes {
|
|||||||
const val BLACKLIST = "settings/blacklist"
|
const val BLACKLIST = "settings/blacklist"
|
||||||
const val SEARCH = "search"
|
const val SEARCH = "search"
|
||||||
const val MY_PROFILE = "my_profile"
|
const val MY_PROFILE = "my_profile"
|
||||||
|
const val STORAGE = "settings/storage"
|
||||||
|
|
||||||
fun codeVerification(login: String) = "code_verification/$login"
|
fun codeVerification(login: String) = "code_verification/$login"
|
||||||
fun chat(chatId: String) = "chat/$chatId"
|
fun chat(chatId: String) = "chat/$chatId"
|
||||||
@ -100,6 +102,9 @@ fun AppNavGraph(
|
|||||||
onNavigateToBlacklist = {
|
onNavigateToBlacklist = {
|
||||||
navController.navigate(Routes.BLACKLIST)
|
navController.navigate(Routes.BLACKLIST)
|
||||||
},
|
},
|
||||||
|
onNavigateToStorage = {
|
||||||
|
navController.navigate(Routes.STORAGE)
|
||||||
|
},
|
||||||
onNavigateToSearch = {
|
onNavigateToSearch = {
|
||||||
navController.navigate(Routes.SEARCH)
|
navController.navigate(Routes.SEARCH)
|
||||||
},
|
},
|
||||||
@ -179,6 +184,13 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(Routes.STORAGE) {
|
||||||
|
StorageScreen(
|
||||||
|
onNavigateBack = {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(Routes.MY_PROFILE) {
|
composable(Routes.MY_PROFILE) {
|
||||||
ProfileScreen(
|
ProfileScreen(
|
||||||
onNavigateBack = {
|
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.Add
|
||||||
import androidx.compose.material.icons.filled.Block
|
import androidx.compose.material.icons.filled.Block
|
||||||
import androidx.compose.material.icons.filled.Close
|
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.Lock
|
||||||
import androidx.compose.material.icons.filled.PhoneAndroid
|
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||||
import androidx.compose.material.icons.filled.Shield
|
import androidx.compose.material.icons.filled.Shield
|
||||||
@ -39,6 +40,7 @@ fun SettingsScreen(
|
|||||||
onNavigateToSessions: () -> Unit,
|
onNavigateToSessions: () -> Unit,
|
||||||
onNavigateToChangePassword: () -> Unit,
|
onNavigateToChangePassword: () -> Unit,
|
||||||
onNavigateToBlacklist: () -> Unit,
|
onNavigateToBlacklist: () -> Unit,
|
||||||
|
onNavigateToStorage: () -> Unit,
|
||||||
onNavigateToProfile: () -> Unit,
|
onNavigateToProfile: () -> Unit,
|
||||||
onAccountSwitched: () -> Unit,
|
onAccountSwitched: () -> Unit,
|
||||||
onAddAccount: () -> Unit,
|
onAddAccount: () -> Unit,
|
||||||
@ -153,6 +155,12 @@ fun SettingsScreen(
|
|||||||
subtitle = "Blocked users",
|
subtitle = "Blocked users",
|
||||||
onClick = onNavigateToBlacklist
|
onClick = onNavigateToBlacklist
|
||||||
)
|
)
|
||||||
|
SettingsMenuItem(
|
||||||
|
icon = Icons.Default.FolderOpen,
|
||||||
|
title = "Storage",
|
||||||
|
subtitle = "Cache and media usage",
|
||||||
|
onClick = onNavigateToStorage
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user