mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Clear cache on logout
Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
+8
@@ -19,3 +19,11 @@ actual suspend fun wipeAttachmentCacheDirectories() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun wipeInstanceAuxiliaryCacheDirectory(instanceId: String) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val safe = instanceId.trim().replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
if (safe.isEmpty()) return@withContext
|
||||||
|
File(UtilsLibrary.context.cacheDir, "fromchat/instances/$safe").deleteRecursively()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,12 +52,11 @@ import ru.fromchat.api.instance.InstanceIdGuard
|
|||||||
import ru.fromchat.api.instance.InstanceIdResolveResult
|
import ru.fromchat.api.instance.InstanceIdResolveResult
|
||||||
import ru.fromchat.api.instance.resolveInstanceId
|
import ru.fromchat.api.instance.resolveInstanceId
|
||||||
import ru.fromchat.api.local.WebSocketManager
|
import ru.fromchat.api.local.WebSocketManager
|
||||||
import ru.fromchat.api.local.db.store.MessageRepository
|
|
||||||
import ru.fromchat.api.local.send.cancelOutboxProcessing
|
|
||||||
import ru.fromchat.api.local.cache.CacheContext
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
import ru.fromchat.api.local.cache.readOutboundFileBytes
|
import ru.fromchat.api.local.cache.readOutboundFileBytes
|
||||||
|
import ru.fromchat.api.local.db.clearAccountCacheOnLogout
|
||||||
|
import ru.fromchat.api.local.db.store.MessageRepository
|
||||||
import ru.fromchat.api.local.db.store.ProfileCache
|
import ru.fromchat.api.local.db.store.ProfileCache
|
||||||
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
|
||||||
import ru.fromchat.api.local.download.streamEncryptedFileToDisk
|
import ru.fromchat.api.local.download.streamEncryptedFileToDisk
|
||||||
import ru.fromchat.api.local.send.scheduleOutboxProcessing
|
import ru.fromchat.api.local.send.scheduleOutboxProcessing
|
||||||
import ru.fromchat.api.schema.calls.CallSignalingLiveKitControl
|
import ru.fromchat.api.schema.calls.CallSignalingLiveKitControl
|
||||||
@@ -115,8 +114,6 @@ import ru.fromchat.api.schema.websocket.types.DmTypingData
|
|||||||
import ru.fromchat.api.schema.websocket.types.SubscribeStatusData
|
import ru.fromchat.api.schema.websocket.types.SubscribeStatusData
|
||||||
import ru.fromchat.config.ServerConfig
|
import ru.fromchat.config.ServerConfig
|
||||||
import ru.fromchat.config.Settings
|
import ru.fromchat.config.Settings
|
||||||
import ru.fromchat.ui.chat.panels.dm.DmPanelCache
|
|
||||||
import ru.fromchat.ui.chat.utils.PublicChatPanelCache
|
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -236,13 +233,10 @@ object ApiClient {
|
|||||||
val path = response.call.request.url.encodedPath
|
val path = response.call.request.url.encodedPath
|
||||||
val isCredentialCheck = path.endsWith("/login") || path.endsWith("/register")
|
val isCredentialCheck = path.endsWith("/login") || path.endsWith("/register")
|
||||||
if (!isCredentialCheck) {
|
if (!isCredentialCheck) {
|
||||||
token = null
|
MainScope().launch {
|
||||||
user = null
|
runCatching { WebSocketManager.disconnect() }
|
||||||
clearSuspensionState()
|
runCatching { clearLocalSession() }
|
||||||
onAuthError?.let {
|
onAuthError?.invoke()
|
||||||
MainScope().launch {
|
|
||||||
it()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,6 +364,7 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
Settings.lastKnownServerInstanceId = id
|
Settings.lastKnownServerInstanceId = id
|
||||||
CacheContext.setActiveInstance(id, user?.id)
|
CacheContext.setActiveInstance(id, user?.id)
|
||||||
|
PublicChatProfileSync.ensureStarted()
|
||||||
scheduleOutboxProcessing(id)
|
scheduleOutboxProcessing(id)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@@ -1150,6 +1145,17 @@ object ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clears in-memory and on-disk partial download state on logout. */
|
||||||
|
suspend fun clearAllDownloadCachesOnLogout() {
|
||||||
|
val keys = partialDownloadMetaCache.keys.toList()
|
||||||
|
keys.forEach { clearPartialEncryptedDownload(it) }
|
||||||
|
partialDownloadMetaCache.clear()
|
||||||
|
pausedDownloadIndexCache = emptySet()
|
||||||
|
pausedDownloadIndexPath()?.let { path ->
|
||||||
|
runCatching { PlatformFileSystem.delete(path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun pausedDownloadIndexPath(): String? {
|
private fun pausedDownloadIndexPath(): String? {
|
||||||
val dir = encryptedDownloadsDir() ?: return null
|
val dir = encryptedDownloadsDir() ?: return null
|
||||||
return "$dir/paused_keys.txt"
|
return "$dir/paused_keys.txt"
|
||||||
@@ -1383,10 +1389,7 @@ object ApiClient {
|
|||||||
*/
|
*/
|
||||||
suspend fun clearLocalSession() {
|
suspend fun clearLocalSession() {
|
||||||
val instanceId = runCatching { CacheContext.activeInstanceId.value.trim() }.getOrDefault("")
|
val instanceId = runCatching { CacheContext.activeInstanceId.value.trim() }.getOrDefault("")
|
||||||
if (instanceId.isNotEmpty()) {
|
runCatching { clearAccountCacheOnLogout(instanceId) }
|
||||||
runCatching { cancelOutboxProcessing(instanceId) }
|
|
||||||
runCatching { MessageRepository.purgeAllPendingForInstance() }
|
|
||||||
}
|
|
||||||
val uid = user?.id
|
val uid = user?.id
|
||||||
secureSettings.remove("auth_token")
|
secureSettings.remove("auth_token")
|
||||||
settings.remove("user_info")
|
settings.remove("user_info")
|
||||||
@@ -1399,10 +1402,6 @@ object ApiClient {
|
|||||||
uid?.let { UpdateSyncManager.clearPersistedSeqForUser(it) }
|
uid?.let { UpdateSyncManager.clearPersistedSeqForUser(it) }
|
||||||
UpdateSyncManager.resetInMemoryOnLogout()
|
UpdateSyncManager.resetInMemoryOnLogout()
|
||||||
runCatching { IdentityKeyManager.clearLocalKeys() }
|
runCatching { IdentityKeyManager.clearLocalKeys() }
|
||||||
runCatching { ProfileCache.clear() }
|
|
||||||
runCatching { DmPanelCache.clearAll() }
|
|
||||||
runCatching { PublicChatProfileCache.clear() }
|
|
||||||
runCatching { PublicChatPanelCache.clear() }
|
|
||||||
runCatching { CacheContext.clearActive() }
|
runCatching { CacheContext.clearActive() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package ru.fromchat.api
|
|||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -22,6 +23,7 @@ object PublicChatProfileSync {
|
|||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
private var started = false
|
private var started = false
|
||||||
|
private var syncJob: Job? = null
|
||||||
|
|
||||||
fun ensureStarted() {
|
fun ensureStarted() {
|
||||||
if (started) return
|
if (started) return
|
||||||
@@ -31,12 +33,18 @@ object PublicChatProfileSync {
|
|||||||
scope.launch { refreshFromNetworkIfNeeded() }
|
scope.launch { refreshFromNetworkIfNeeded() }
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.launch {
|
syncJob = scope.launch {
|
||||||
runCatching { PublicChatProfileCache.hydrateFromDisk() }
|
runCatching { PublicChatProfileCache.hydrateFromDisk() }
|
||||||
syncUntilLoaded()
|
syncUntilLoaded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetOnLogout() {
|
||||||
|
syncJob?.cancel()
|
||||||
|
syncJob = null
|
||||||
|
started = false
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun refreshFromNetwork(): PublicChatProfile {
|
suspend fun refreshFromNetwork(): PublicChatProfile {
|
||||||
if (ApiClient.token.isNullOrEmpty()) {
|
if (ApiClient.token.isNullOrEmpty()) {
|
||||||
return PublicChatProfileCache.profile
|
return PublicChatProfileCache.profile
|
||||||
|
|||||||
+6
@@ -420,4 +420,10 @@ object DecryptedFileCache {
|
|||||||
|
|
||||||
private fun sanitizeKeyPart(value: String): String =
|
private fun sanitizeKeyPart(value: String): String =
|
||||||
value.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
value.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
|
||||||
|
suspend fun clearMemoryCache() {
|
||||||
|
cacheMutex.withLock {
|
||||||
|
memoryCache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -501,4 +501,10 @@ object DecryptedImageCache {
|
|||||||
private fun invalidatePath(path: String) {
|
private fun invalidatePath(path: String) {
|
||||||
runCatching { PlatformFileSystem.delete(path) }
|
runCatching { PlatformFileSystem.delete(path) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearMemoryCache() {
|
||||||
|
cacheMutex.withLock {
|
||||||
|
memoryCache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -8,6 +8,9 @@ expect suspend fun wipeFromChatCacheDirectory()
|
|||||||
/** Removes decrypted attachment blobs and partial download state outside `fromchat/`. */
|
/** Removes decrypted attachment blobs and partial download state outside `fromchat/`. */
|
||||||
expect suspend fun wipeAttachmentCacheDirectories()
|
expect suspend fun wipeAttachmentCacheDirectories()
|
||||||
|
|
||||||
|
/** Deletes `cacheDir/fromchat/instances/<instanceId>/` auxiliary files (export index, pending saves). */
|
||||||
|
expect suspend fun wipeInstanceAuxiliaryCacheDirectory(instanceId: String)
|
||||||
|
|
||||||
suspend fun wipeAllOnDiskAttachmentCaches() {
|
suspend fun wipeAllOnDiskAttachmentCaches() {
|
||||||
wipeFromChatCacheDirectory()
|
wipeFromChatCacheDirectory()
|
||||||
wipeAttachmentCacheDirectories()
|
wipeAttachmentCacheDirectories()
|
||||||
|
|||||||
+18
-3
@@ -80,16 +80,31 @@ object PendingFileSaveRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun indexPath(): String? {
|
private fun indexPath(): String? = instanceIndexPath(
|
||||||
|
runCatching { CacheContext.requireActiveInstanceId() }.getOrNull() ?: "default",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun instanceIndexPath(instanceId: String): String? {
|
||||||
val base = PlatformFileSystem.getAppCacheDirectory()
|
val base = PlatformFileSystem.getAppCacheDirectory()
|
||||||
if (base.isEmpty()) return null
|
if (base.isEmpty()) return null
|
||||||
val instanceId = runCatching { CacheContext.requireActiveInstanceId() }.getOrNull() ?: "default"
|
val safe = instanceId.trim().replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
val safe = instanceId.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
if (safe.isEmpty()) return null
|
||||||
val dir = "$base/fromchat/instances/$safe"
|
val dir = "$base/fromchat/instances/$safe"
|
||||||
PlatformFileSystem.ensureDirectory(dir)
|
PlatformFileSystem.ensureDirectory(dir)
|
||||||
return "$dir/$INDEX_FILE"
|
return "$dir/$INDEX_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearForInstance(instanceId: String) {
|
||||||
|
val path = instanceIndexPath(instanceId) ?: return
|
||||||
|
mutex.withLock {
|
||||||
|
memory.clear()
|
||||||
|
diskLoaded = false
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
runCatching { PlatformFileSystem.delete(path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun readIndexFromDisk(): List<PendingFileSaveEntry> {
|
private suspend fun readIndexFromDisk(): List<PendingFileSaveEntry> {
|
||||||
val path = indexPath() ?: return emptyList()
|
val path = indexPath() ?: return emptyList()
|
||||||
if (!PlatformFileSystem.exists(path)) return emptyList()
|
if (!PlatformFileSystem.exists(path)) return emptyList()
|
||||||
|
|||||||
@@ -2,18 +2,31 @@ package ru.fromchat.api.local.db
|
|||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.fromchat.api.local.db.store.MessageDatabaseProvider
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.local.send.cancelOutboxProcessing
|
import ru.fromchat.api.PublicChatProfileSync
|
||||||
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
|
||||||
import ru.fromchat.api.local.cache.CacheContext
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
|
import ru.fromchat.api.local.cache.DecryptedFileCache
|
||||||
|
import ru.fromchat.api.local.cache.DecryptedImageCache
|
||||||
|
import ru.fromchat.api.local.cache.PendingFileSaveRegistry
|
||||||
import ru.fromchat.api.local.cache.wipeAttachmentCacheDirectories
|
import ru.fromchat.api.local.cache.wipeAttachmentCacheDirectories
|
||||||
import ru.fromchat.api.local.cache.wipeFromChatCacheDirectory
|
import ru.fromchat.api.local.cache.wipeFromChatCacheDirectory
|
||||||
|
import ru.fromchat.api.local.cache.wipeInstanceAuxiliaryCacheDirectory
|
||||||
|
import ru.fromchat.api.local.db.store.InstanceRegistryStore
|
||||||
|
import ru.fromchat.api.local.db.store.MessageDatabaseProvider
|
||||||
|
import ru.fromchat.api.local.db.store.MessageRepository
|
||||||
|
import ru.fromchat.api.local.db.store.ProfileCache
|
||||||
|
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
||||||
|
import ru.fromchat.api.local.download.AttachmentDownloadNotifier
|
||||||
|
import ru.fromchat.api.local.download.AttachmentDownloadScheduler
|
||||||
|
import ru.fromchat.api.local.download.DownloadedFileRegistry
|
||||||
|
import ru.fromchat.api.local.download.LocalDecodedImageCache
|
||||||
|
import ru.fromchat.api.local.send.cancelOutboxProcessing
|
||||||
import ru.fromchat.ui.chat.panels.dm.DmPanelCache
|
import ru.fromchat.ui.chat.panels.dm.DmPanelCache
|
||||||
import ru.fromchat.ui.chat.utils.PublicChatPanelCache
|
import ru.fromchat.ui.chat.utils.PublicChatPanelCache
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drops the on-disk FromChat cache tree and reopens SQLite on next access.
|
* Drops the on-disk FromChat cache tree and reopens SQLite on next access.
|
||||||
* Call [writeFromChatCacheGeneration] after this when wiping from settings.
|
* Call [ru.fromchat.api.local.cache.writeFromChatCacheGeneration] after this when wiping from settings.
|
||||||
*/
|
*/
|
||||||
suspend fun wipeLocalCacheOnDisk() {
|
suspend fun wipeLocalCacheOnDisk() {
|
||||||
val instanceId = runCatching { CacheContext.activeInstanceId.value.trim() }.getOrDefault("")
|
val instanceId = runCatching { CacheContext.activeInstanceId.value.trim() }.getOrDefault("")
|
||||||
@@ -25,7 +38,39 @@ suspend fun wipeLocalCacheOnDisk() {
|
|||||||
}
|
}
|
||||||
wipeFromChatCacheDirectory()
|
wipeFromChatCacheDirectory()
|
||||||
wipeAttachmentCacheDirectories()
|
wipeAttachmentCacheDirectories()
|
||||||
PublicChatProfileCache.clear()
|
clearInMemoryAccountCaches()
|
||||||
PublicChatPanelCache.clear()
|
}
|
||||||
DmPanelCache.clearAll()
|
|
||||||
|
/**
|
||||||
|
* Clears all per-account cache for the active server instance on logout.
|
||||||
|
* Preserves other server-instance partitions in SQLite for multi-server use.
|
||||||
|
*/
|
||||||
|
suspend fun clearAccountCacheOnLogout(instanceId: String) {
|
||||||
|
val id = instanceId.trim()
|
||||||
|
PublicChatProfileSync.resetOnLogout()
|
||||||
|
if (id.isNotEmpty()) {
|
||||||
|
cancelOutboxProcessing(id)
|
||||||
|
runCatching { MessageRepository.purgeAllPendingForInstance() }
|
||||||
|
runCatching { AttachmentDownloadScheduler.cancelAllOnLogout() }
|
||||||
|
runCatching { InstanceRegistryStore.purgePartition(id) }
|
||||||
|
runCatching { InstanceRegistryStore.clearServerBindingForCurrentConfig() }
|
||||||
|
runCatching { wipeInstanceAuxiliaryCacheDirectory(id) }
|
||||||
|
runCatching { DownloadedFileRegistry.clearForInstance(id) }
|
||||||
|
runCatching { PendingFileSaveRegistry.clearForInstance(id) }
|
||||||
|
}
|
||||||
|
runCatching { ApiClient.clearAllDownloadCachesOnLogout() }
|
||||||
|
wipeAttachmentCacheDirectories()
|
||||||
|
clearInMemoryAccountCaches()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun clearInMemoryAccountCaches() {
|
||||||
|
runCatching { ProfileCache.clear() }
|
||||||
|
runCatching { PublicChatProfileCache.clear() }
|
||||||
|
runCatching { DmPanelCache.clearAll() }
|
||||||
|
PublicChatPanelCache.clear()
|
||||||
|
AttachmentDownloadNotifier.resetOnLogout()
|
||||||
|
DecryptedImageCache.clearMemoryCache()
|
||||||
|
DecryptedFileCache.clearMemoryCache()
|
||||||
|
MessageRepository.resetListPreviewStringsOnLogout()
|
||||||
|
LocalDecodedImageCache.evictPrefix("img_")
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -9,6 +9,7 @@ import ru.fromchat.api.instance.configKey
|
|||||||
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
||||||
import ru.fromchat.ui.chat.utils.PublicChatPanelCache
|
import ru.fromchat.ui.chat.utils.PublicChatPanelCache
|
||||||
import ru.fromchat.ui.chat.panels.dm.DmPanelCache
|
import ru.fromchat.ui.chat.panels.dm.DmPanelCache
|
||||||
|
import ru.fromchat.config.Settings
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
object InstanceRegistryStore {
|
object InstanceRegistryStore {
|
||||||
@@ -111,4 +112,12 @@ object InstanceRegistryStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearServerBindingForCurrentConfig() {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
MessageDatabaseProvider.withDatabaseRecover {
|
||||||
|
db.messageDatabaseQueries.deleteServerBinding(Settings.serverConfig.configKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-7
@@ -91,10 +91,10 @@ object MessageCacheStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadPublicMessages(): List<Message> =
|
suspend fun loadPublicMessages(): List<Message> =
|
||||||
loadMessages(conversationIdForPublic())
|
ProfileCache.enrichPublicMessagesForDisplay(loadMessages(conversationIdForPublic()))
|
||||||
|
|
||||||
suspend fun loadRecentPublicMessages(limit: Long): List<Message> =
|
suspend fun loadRecentPublicMessages(limit: Long): List<Message> =
|
||||||
loadRecentMessages(conversationIdForPublic(), limit)
|
ProfileCache.enrichPublicMessagesForDisplay(loadRecentMessages(conversationIdForPublic(), limit))
|
||||||
|
|
||||||
fun loadRecentPublicMessagesImmediate(instanceId: String, limit: Long = 128): List<Message> {
|
fun loadRecentPublicMessagesImmediate(instanceId: String, limit: Long = 128): List<Message> {
|
||||||
if (instanceId.isBlank()) return emptyList()
|
if (instanceId.isBlank()) return emptyList()
|
||||||
@@ -105,11 +105,13 @@ object MessageCacheStore {
|
|||||||
.map { row: DbMessage -> row.toAppMessage() }
|
.map { row: DbMessage -> row.toAppMessage() }
|
||||||
.reversed()
|
.reversed()
|
||||||
val withoutSuperseded = dropSupersededOptimisticMessages(raw, ApiClient.user?.id)
|
val withoutSuperseded = dropSupersededOptimisticMessages(raw, ApiClient.user?.id)
|
||||||
return sortMessagesForChatDisplay(
|
return ProfileCache.enrichPublicMessagesForDisplay(
|
||||||
validatedOrEmpty(
|
sortMessagesForChatDisplay(
|
||||||
convId,
|
validatedOrEmpty(
|
||||||
dedupeMessagesByClientId(
|
convId,
|
||||||
enrichQueuedOutboundUi(withoutSuperseded, convId),
|
dedupeMessagesByClientId(
|
||||||
|
enrichQueuedOutboundUi(withoutSuperseded, convId),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -150,6 +152,7 @@ object MessageCacheStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun replacePublicMessages(messages: List<Message>) {
|
suspend fun replacePublicMessages(messages: List<Message>) {
|
||||||
|
ProfileCache.mergePreviewFromPublicMessages(messages)
|
||||||
conversationIdForPublic().let {
|
conversationIdForPublic().let {
|
||||||
replaceMessages(
|
replaceMessages(
|
||||||
it,
|
it,
|
||||||
@@ -225,6 +228,7 @@ object MessageCacheStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun upsertPublicMessage(message: Message) {
|
suspend fun upsertPublicMessage(message: Message) {
|
||||||
|
ProfileCache.mergePreviewFromPublicMessage(message)
|
||||||
upsertSingle(conversationIdForPublic(), message)
|
upsertSingle(conversationIdForPublic(), message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +284,7 @@ object MessageCacheStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun confirmPublicMessage(clientMessageId: String, confirmed: Message) {
|
suspend fun confirmPublicMessage(clientMessageId: String, confirmed: Message) {
|
||||||
|
ProfileCache.mergePreviewFromPublicMessage(confirmed)
|
||||||
confirmMessage(conversationIdForPublic(), clientMessageId, confirmed)
|
confirmMessage(conversationIdForPublic(), clientMessageId, confirmed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,4 +149,8 @@ object MessageRepository {
|
|||||||
MessageCacheStore.pruneEmptyConversations()
|
MessageCacheStore.pruneEmptyConversations()
|
||||||
|
|
||||||
suspend fun clearAllCache() = MessageCacheStore.clearAll()
|
suspend fun clearAllCache() = MessageCacheStore.clearAll()
|
||||||
|
|
||||||
|
fun resetListPreviewStringsOnLogout() {
|
||||||
|
MessageCacheStore.listPreviewStrings = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.schema.messages.Message
|
import ru.fromchat.api.schema.messages.Message
|
||||||
import ru.fromchat.api.schema.user.User
|
import ru.fromchat.api.schema.user.User
|
||||||
import ru.fromchat.api.schema.user.profile.UserProfile
|
import ru.fromchat.api.schema.user.profile.UserProfile
|
||||||
@@ -182,7 +183,8 @@ object ProfileCache {
|
|||||||
val existing = get(uid)
|
val existing = get(uid)
|
||||||
if (existing != null && !existing.isClientPreviewOnly) return
|
if (existing != null && !existing.isClientPreviewOnly) return
|
||||||
|
|
||||||
val uname = message.username.ifBlank { existing?.username ?: return }
|
val uname = message.username.trim().ifBlank { existing?.username?.trim().orEmpty() }
|
||||||
|
if (uname.isBlank()) return
|
||||||
val display = existing?.displayName?.takeIf { it.isNotBlank() } ?: uname
|
val display = existing?.displayName?.takeIf { it.isNotBlank() } ?: uname
|
||||||
val pic = message.profile_picture?.takeIf { it.isNotBlank() } ?: existing?.profilePicture
|
val pic = message.profile_picture?.takeIf { it.isNotBlank() } ?: existing?.profilePicture
|
||||||
|
|
||||||
@@ -206,6 +208,43 @@ object ProfileCache {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun mergePreviewFromPublicMessages(messages: Iterable<Message>) {
|
||||||
|
messages.forEach(::mergePreviewFromPublicMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills blank sender fields on a public-chat [Message] from [ProfileCache] or the current user.
|
||||||
|
*/
|
||||||
|
fun enrichPublicMessageForDisplay(
|
||||||
|
message: Message,
|
||||||
|
currentUserId: Int? = ApiClient.user?.id,
|
||||||
|
): Message {
|
||||||
|
val self = currentUserId
|
||||||
|
if (self != null && message.user_id == self) {
|
||||||
|
val user = ApiClient.user
|
||||||
|
return message.copy(
|
||||||
|
username = message.username.trim().ifBlank { user?.username.orEmpty() },
|
||||||
|
profile_picture = message.profile_picture?.takeIf { it.isNotBlank() }
|
||||||
|
?: user?.profile_picture,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val profile = get(message.user_id)
|
||||||
|
return message.copy(
|
||||||
|
username = message.username.trim().ifBlank {
|
||||||
|
profile?.visibleUsername(self).orEmpty()
|
||||||
|
},
|
||||||
|
profile_picture = message.profile_picture?.takeIf { it.isNotBlank() }
|
||||||
|
?: profile?.profilePicture,
|
||||||
|
verified = message.verified ?: profile?.verified,
|
||||||
|
verificationStatus = message.verificationStatus ?: profile?.verificationStatus,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enrichPublicMessagesForDisplay(
|
||||||
|
messages: List<Message>,
|
||||||
|
currentUserId: Int? = ApiClient.user?.id,
|
||||||
|
): List<Message> = messages.map { enrichPublicMessageForDisplay(it, currentUserId) }
|
||||||
|
|
||||||
fun onActiveInstanceChanged(instanceId: String) {
|
fun onActiveInstanceChanged(instanceId: String) {
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
persistMutex.withLock {
|
persistMutex.withLock {
|
||||||
|
|||||||
+7
@@ -369,4 +369,11 @@ object AttachmentDownloadNotifier {
|
|||||||
val id = keys.firstOrNull() ?: return DownloadProgressThrottle()
|
val id = keys.firstOrNull() ?: return DownloadProgressThrottle()
|
||||||
return progressThrottleByKey.getOrPut(id) { DownloadProgressThrottle() }
|
return progressThrottleByKey.getOrPut(id) { DownloadProgressThrottle() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetOnLogout() {
|
||||||
|
_progressPercentByKey.value = emptyMap()
|
||||||
|
_failedKeys.value = emptySet()
|
||||||
|
_cancelledKeys.value = emptySet()
|
||||||
|
progressThrottleByKey.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -202,6 +202,13 @@ object AttachmentDownloadScheduler {
|
|||||||
}.thenBy { it.enqueuedAt },
|
}.thenBy { it.enqueuedAt },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun cancelAllOnLogout() {
|
||||||
|
val keys = mutex.withLock {
|
||||||
|
(activeJobs.keys + waiting.map { it.storageKey } + keyToDeferred.keys).toSet()
|
||||||
|
}
|
||||||
|
keys.forEach { cancel(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun checkAttachmentDownloadActive(storageKey: String) {
|
internal fun checkAttachmentDownloadActive(storageKey: String) {
|
||||||
|
|||||||
+18
-3
@@ -142,16 +142,31 @@ object DownloadedFileRegistry {
|
|||||||
private fun sanitizeKeyPart(value: String): String =
|
private fun sanitizeKeyPart(value: String): String =
|
||||||
value.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
value.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
|
||||||
private fun indexPath(): String? {
|
private fun indexPath(): String? = instanceIndexPath(
|
||||||
|
runCatching { CacheContext.requireActiveInstanceId() }.getOrNull() ?: "default",
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun instanceIndexPath(instanceId: String): String? {
|
||||||
val base = PlatformFileSystem.getAppCacheDirectory()
|
val base = PlatformFileSystem.getAppCacheDirectory()
|
||||||
if (base.isEmpty()) return null
|
if (base.isEmpty()) return null
|
||||||
val instanceId = runCatching { CacheContext.requireActiveInstanceId() }.getOrNull() ?: "default"
|
val safe = instanceId.trim().replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
val safe = instanceId.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
if (safe.isEmpty()) return null
|
||||||
val dir = "$base/fromchat/instances/$safe"
|
val dir = "$base/fromchat/instances/$safe"
|
||||||
PlatformFileSystem.ensureDirectory(dir)
|
PlatformFileSystem.ensureDirectory(dir)
|
||||||
return "$dir/$INDEX_FILE"
|
return "$dir/$INDEX_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun clearForInstance(instanceId: String) {
|
||||||
|
val path = instanceIndexPath(instanceId) ?: return
|
||||||
|
mutex.withLock {
|
||||||
|
memory.clear()
|
||||||
|
diskIndexLoaded = false
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
runCatching { PlatformFileSystem.delete(path) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun readIndexFromDisk(): Map<String, String> {
|
private suspend fun readIndexFromDisk(): Map<String, String> {
|
||||||
val path = indexPath() ?: return emptyMap()
|
val path = indexPath() ?: return emptyMap()
|
||||||
if (!PlatformFileSystem.exists(path)) return emptyMap()
|
if (!PlatformFileSystem.exists(path)) return emptyMap()
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ import ru.fromchat.api.DeferredStartupNetwork
|
|||||||
import ru.fromchat.api.PublicChatProfileSync
|
import ru.fromchat.api.PublicChatProfileSync
|
||||||
import ru.fromchat.api.UpdateSyncManager
|
import ru.fromchat.api.UpdateSyncManager
|
||||||
import ru.fromchat.api.calls.CallStore
|
import ru.fromchat.api.calls.CallStore
|
||||||
|
import ru.fromchat.api.instance.bootstrapSessionInstance
|
||||||
import ru.fromchat.api.instance.bootstrapSessionOnStartup
|
import ru.fromchat.api.instance.bootstrapSessionOnStartup
|
||||||
import ru.fromchat.api.instance.logoutIfInstanceUnsupported
|
import ru.fromchat.api.instance.logoutIfInstanceUnsupported
|
||||||
|
import ru.fromchat.api.instance.scheduleSessionInstanceNetworkRefresh
|
||||||
import ru.fromchat.api.local.WebSocketManager
|
import ru.fromchat.api.local.WebSocketManager
|
||||||
import ru.fromchat.api.local.cache.CacheContext
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
import ru.fromchat.api.local.cache.ensureFromChatCacheGeneration
|
import ru.fromchat.api.local.cache.ensureFromChatCacheGeneration
|
||||||
@@ -427,6 +429,16 @@ fun App(
|
|||||||
composable("auth") {
|
composable("auth") {
|
||||||
AuthScreen(
|
AuthScreen(
|
||||||
onAuthSuccess = {
|
onAuthSuccess = {
|
||||||
|
MainScope().launch {
|
||||||
|
runCatching {
|
||||||
|
bootstrapSessionInstance(
|
||||||
|
hasToken = true,
|
||||||
|
forceNetwork = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PublicChatProfileSync.ensureStarted()
|
||||||
|
scheduleSessionInstanceNetworkRefresh()
|
||||||
|
}
|
||||||
WebSocketManager.connect(forceRestart = true)
|
WebSocketManager.connect(forceRestart = true)
|
||||||
navController.navigateAndWipeBackStack("chat")
|
navController.navigateAndWipeBackStack("chat")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
import com.pr0gramm3r101.utils.crypto.deriveAuthSecret
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
@@ -22,6 +23,8 @@ import org.jetbrains.compose.resources.stringResource
|
|||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.api.ApiClient
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.crypto.IdentityKeyManager
|
import ru.fromchat.api.crypto.IdentityKeyManager
|
||||||
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
|
import ru.fromchat.api.local.db.clearAccountCacheOnLogout
|
||||||
import ru.fromchat.api.instance.ServerProbeResult
|
import ru.fromchat.api.instance.ServerProbeResult
|
||||||
import ru.fromchat.api.instance.probeServer
|
import ru.fromchat.api.instance.probeServer
|
||||||
import ru.fromchat.api.schema.core.ErrorResponse
|
import ru.fromchat.api.schema.core.ErrorResponse
|
||||||
@@ -98,6 +101,8 @@ private suspend fun fullLogin(
|
|||||||
password: String,
|
password: String,
|
||||||
request: suspend () -> LoginResponse,
|
request: suspend () -> LoginResponse,
|
||||||
) {
|
) {
|
||||||
|
val previousInstanceId = runCatching { CacheContext.activeInstanceId.value.trim() }.getOrDefault("")
|
||||||
|
runCatching { clearAccountCacheOnLogout(previousInstanceId) }
|
||||||
ApiClient.clearMemorySession()
|
ApiClient.clearMemorySession()
|
||||||
|
|
||||||
val response = request()
|
val response = request()
|
||||||
@@ -257,6 +262,31 @@ fun AuthScreen(
|
|||||||
bio = ""
|
bio = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(flowState.pagerState) {
|
||||||
|
var settledPage = flowState.pagerState.currentPage
|
||||||
|
snapshotFlow { flowState.pagerState.currentPage }
|
||||||
|
.collect { page ->
|
||||||
|
if (page < settledPage) {
|
||||||
|
when (page) {
|
||||||
|
AuthFlowStep.Username.ordinal -> {
|
||||||
|
password = ""
|
||||||
|
confirmPassword = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthFlowStep.Password.ordinal -> {
|
||||||
|
password = ""
|
||||||
|
confirmPassword = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthFlowStep.ConfirmPassword.ordinal -> {
|
||||||
|
confirmPassword = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settledPage = page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExpressiveStepFlowScaffold(
|
ExpressiveStepFlowScaffold(
|
||||||
flowState = flowState,
|
flowState = flowState,
|
||||||
pages = listOf(
|
pages = listOf(
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ fun ChatInput(
|
|||||||
enabled = !isReadOnly,
|
enabled = !isReadOnly,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
.align(Alignment.Bottom),
|
.align(Alignment.Bottom),
|
||||||
textStyle = inputTextStyle,
|
textStyle = inputTextStyle,
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
@@ -452,6 +453,7 @@ fun ChatInput(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.defaultMinSize(minHeight = ChatInputIconSlotSize)
|
.defaultMinSize(minHeight = ChatInputIconSlotSize)
|
||||||
|
.animateContentSize()
|
||||||
.align(Alignment.BottomStart),
|
.align(Alignment.BottomStart),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
@@ -469,9 +471,7 @@ fun ChatInput(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.animateContentSize()) {
|
innerTextField()
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package ru.fromchat.ui.chat
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
|
import ru.fromchat.api.ApiClient
|
||||||
import ru.fromchat.api.local.db.store.ProfileCache
|
import ru.fromchat.api.local.db.store.ProfileCache
|
||||||
import ru.fromchat.api.schema.messages.Message
|
import ru.fromchat.api.schema.messages.Message
|
||||||
import ru.fromchat.api.local.db.store.visibleUsername
|
import ru.fromchat.api.local.db.store.visibleUsername
|
||||||
|
import ru.fromchat.api.local.db.store.visibleDisplayName
|
||||||
import ru.fromchat.message_sender_you
|
import ru.fromchat.message_sender_you
|
||||||
import ru.fromchat.user_fallback
|
import ru.fromchat.user_fallback
|
||||||
|
|
||||||
@@ -38,3 +40,27 @@ fun messageDisplayUsername(message: Message, currentUserId: Int?): String {
|
|||||||
}
|
}
|
||||||
return message.username
|
return message.username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun messageSenderProfilePicture(
|
||||||
|
message: Message,
|
||||||
|
currentUserId: Int? = ApiClient.user?.id,
|
||||||
|
): String? {
|
||||||
|
if (currentUserId != null && message.user_id == currentUserId) {
|
||||||
|
return message.profile_picture?.takeIf { it.isNotBlank() }
|
||||||
|
?: ApiClient.user?.profile_picture?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
return message.profile_picture?.takeIf { it.isNotBlank() }
|
||||||
|
?: ProfileCache.get(message.user_id)?.profilePicture?.takeIf { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun messageSenderAvatarLabel(
|
||||||
|
message: Message,
|
||||||
|
currentUserId: Int? = ApiClient.user?.id,
|
||||||
|
): String {
|
||||||
|
if (currentUserId != null && message.user_id == currentUserId) {
|
||||||
|
return ApiClient.user?.username?.takeIf { it.isNotBlank() }.orEmpty()
|
||||||
|
}
|
||||||
|
return message.username.trim().ifBlank {
|
||||||
|
ProfileCache.get(message.user_id)?.visibleDisplayName(currentUserId).orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ import com.pr0gramm3r101.utils.conditional
|
|||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
import ru.fromchat.api.local.cache.DecryptedImageCache
|
import ru.fromchat.api.local.cache.DecryptedImageCache
|
||||||
|
import ru.fromchat.api.local.db.store.ProfileCache
|
||||||
|
import ru.fromchat.api.local.db.store.visibleUsername
|
||||||
import ru.fromchat.api.local.messages.formatMessageTimeLocal
|
import ru.fromchat.api.local.messages.formatMessageTimeLocal
|
||||||
import ru.fromchat.api.local.messages.isQueuedOutbound
|
import ru.fromchat.api.local.messages.isQueuedOutbound
|
||||||
import ru.fromchat.api.schema.messages.Message
|
import ru.fromchat.api.schema.messages.Message
|
||||||
@@ -123,6 +125,11 @@ fun MessageItem(
|
|||||||
val editedSuffix = stringResource(Res.string.message_edited_suffix)
|
val editedSuffix = stringResource(Res.string.message_edited_suffix)
|
||||||
val sendFailedLabel = stringResource(Res.string.message_send_failed)
|
val sendFailedLabel = stringResource(Res.string.message_send_failed)
|
||||||
val displayUsername = messageDisplayUsername(message, currentUserId)
|
val displayUsername = messageDisplayUsername(message, currentUserId)
|
||||||
|
val senderProfile = ProfileCache.get(message.user_id)
|
||||||
|
val avatarPictureUrl = senderProfile?.profilePicture?.takeIf { it.isNotBlank() }
|
||||||
|
?: message.profile_picture
|
||||||
|
val avatarDisplayName = senderProfile?.visibleUsername(currentUserId)?.takeIf { it.isNotBlank() }
|
||||||
|
?: message.username
|
||||||
val senderVerificationStatus = resolveVerificationStatus(message.user_id, message)
|
val senderVerificationStatus = resolveVerificationStatus(message.user_id, message)
|
||||||
val replyRef = message.reply_to
|
val replyRef = message.reply_to
|
||||||
|
|
||||||
@@ -184,8 +191,8 @@ fun MessageItem(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Avatar(
|
Avatar(
|
||||||
profilePictureUrl = message.profile_picture,
|
profilePictureUrl = avatarPictureUrl,
|
||||||
displayName = message.username,
|
displayName = avatarDisplayName,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-4
@@ -12,6 +12,7 @@ import ru.fromchat.api.ApiClient
|
|||||||
import ru.fromchat.api.local.cache.CacheContext
|
import ru.fromchat.api.local.cache.CacheContext
|
||||||
import ru.fromchat.api.local.cache.DecryptedImageCache
|
import ru.fromchat.api.local.cache.DecryptedImageCache
|
||||||
import ru.fromchat.api.local.db.store.MessageCacheStore
|
import ru.fromchat.api.local.db.store.MessageCacheStore
|
||||||
|
import ru.fromchat.api.local.db.store.ProfileCache
|
||||||
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
import ru.fromchat.api.local.db.store.PublicChatProfileCache
|
||||||
import ru.fromchat.api.local.db.store.MessageRepository
|
import ru.fromchat.api.local.db.store.MessageRepository
|
||||||
import ru.fromchat.api.local.messages.GENERAL_PUBLIC_GROUP_ID
|
import ru.fromchat.api.local.messages.GENERAL_PUBLIC_GROUP_ID
|
||||||
@@ -63,6 +64,25 @@ class PublicChatPanel(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun mergePublicSenderFieldsFromNetwork(
|
||||||
|
shown: List<Message>,
|
||||||
|
fromNetwork: List<Message>,
|
||||||
|
): List<Message> {
|
||||||
|
val byId = fromNetwork.associateBy { it.id }
|
||||||
|
return shown.map { message ->
|
||||||
|
val fresh = byId[message.id] ?: return@map message
|
||||||
|
ProfileCache.mergePreviewFromPublicMessage(fresh)
|
||||||
|
ProfileCache.enrichPublicMessageForDisplay(
|
||||||
|
message.copy(
|
||||||
|
username = fresh.username,
|
||||||
|
profile_picture = fresh.profile_picture,
|
||||||
|
verified = fresh.verified,
|
||||||
|
verificationStatus = fresh.verificationStatus,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override val supportsNavigateToSenderProfile: Boolean
|
override val supportsNavigateToSenderProfile: Boolean
|
||||||
get() = true
|
get() = true
|
||||||
|
|
||||||
@@ -187,7 +207,9 @@ class PublicChatPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ingestIncomingPublicMessage(newMsg: Message) {
|
private suspend fun ingestIncomingPublicMessage(newMsg: Message) {
|
||||||
addMessage(newMsg)
|
ProfileCache.mergePreviewFromPublicMessage(newMsg)
|
||||||
|
val displayMessage = ProfileCache.enrichPublicMessageForDisplay(newMsg)
|
||||||
|
addMessage(displayMessage)
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
MessageCacheStore.upsertPublicMessage(newMsg)
|
MessageCacheStore.upsertPublicMessage(newMsg)
|
||||||
}
|
}
|
||||||
@@ -252,17 +274,24 @@ class PublicChatPanel(
|
|||||||
val response = responseResult.getOrNull()
|
val response = responseResult.getOrNull()
|
||||||
|
|
||||||
if (response != null && response.messages.isNotEmpty()) {
|
if (response != null && response.messages.isNotEmpty()) {
|
||||||
|
ProfileCache.mergePreviewFromPublicMessages(response.messages)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val shown = _state.messages
|
val shown = _state.messages
|
||||||
if (shown.isNotEmpty() && !publicHistoryDiffersForUi(shown, response.messages)) {
|
if (shown.isNotEmpty() && !publicHistoryDiffersForUi(shown, response.messages)) {
|
||||||
Logger.d("PublicChatPanel", "Network history matches UI; skip clear/re-add")
|
Logger.d("PublicChatPanel", "Network history matches UI; skip clear/re-add")
|
||||||
|
val withSenders = mergePublicSenderFieldsFromNetwork(shown, response.messages)
|
||||||
|
if (withSenders != shown) {
|
||||||
|
updateState { it.copy(messages = sortMessagesForChatDisplay(withSenders)) }
|
||||||
|
}
|
||||||
if (_state.hasMoreMessages) setHasMoreMessages(false)
|
if (_state.hasMoreMessages) setHasMoreMessages(false)
|
||||||
if (_state.isLoading) setLoading(false)
|
if (_state.isLoading) setLoading(false)
|
||||||
} else {
|
} else {
|
||||||
batchStateUpdates {
|
batchStateUpdates {
|
||||||
val merged = mergeNetworkHistoryWithShown(shown, response.messages)
|
val merged = mergeNetworkHistoryWithShown(shown, response.messages)
|
||||||
clearMessages()
|
clearMessages()
|
||||||
addMessages(merged)
|
addMessages(
|
||||||
|
ProfileCache.enrichPublicMessagesForDisplay(merged),
|
||||||
|
)
|
||||||
setHasMoreMessages(false) // TODO: Implement has_more from API
|
setHasMoreMessages(false) // TODO: Implement has_more from API
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -315,10 +344,11 @@ class PublicChatPanel(
|
|||||||
ApiClient.getMessages(limit = 50, beforeId = oldestMessage.id)
|
ApiClient.getMessages(limit = 50, beforeId = oldestMessage.id)
|
||||||
}
|
}
|
||||||
if (response.messages.isNotEmpty()) {
|
if (response.messages.isNotEmpty()) {
|
||||||
// Prepend older messages (they come in reverse chronological order)
|
ProfileCache.mergePreviewFromPublicMessages(response.messages)
|
||||||
|
val older = ProfileCache.enrichPublicMessagesForDisplay(response.messages.reversed())
|
||||||
updateState { currentState ->
|
updateState { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
messages = response.messages.reversed() + currentState.messages
|
messages = older + currentState.messages
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ import ru.fromchat.chat_preview_image
|
|||||||
import ru.fromchat.chat_preview_image_emoji
|
import ru.fromchat.chat_preview_image_emoji
|
||||||
import ru.fromchat.chats_selected_count
|
import ru.fromchat.chats_selected_count
|
||||||
import ru.fromchat.config.ServerConfig
|
import ru.fromchat.config.ServerConfig
|
||||||
|
import ru.fromchat.public_chat
|
||||||
import ru.fromchat.search_title
|
import ru.fromchat.search_title
|
||||||
import ru.fromchat.status_connecting
|
import ru.fromchat.status_connecting
|
||||||
import ru.fromchat.status_updating
|
import ru.fromchat.status_updating
|
||||||
@@ -517,7 +518,7 @@ fun ChatsTab(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(serverConfig, activeInstanceId) {
|
LaunchedEffect(serverConfig, activeInstanceId, connectionStatus) {
|
||||||
if (activeInstanceId.isBlank()) return@LaunchedEffect
|
if (activeInstanceId.isBlank()) return@LaunchedEffect
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -547,7 +548,9 @@ fun ChatsTab(
|
|||||||
val selectedCountTitle = stringResource(Res.string.chats_selected_count, selectedCount)
|
val selectedCountTitle = stringResource(Res.string.chats_selected_count, selectedCount)
|
||||||
val suspendBannerTitle = stringResource(Res.string.suspend_chat_banner_message)
|
val suspendBannerTitle = stringResource(Res.string.suspend_chat_banner_message)
|
||||||
val suspendDefaultReason = stringResource(Res.string.suspended_default_reason)
|
val suspendDefaultReason = stringResource(Res.string.suspended_default_reason)
|
||||||
|
val publicChatFallbackTitle = stringResource(Res.string.public_chat)
|
||||||
val publicChatTitle = publicChatProfile?.title?.takeIf { it.isNotBlank() }
|
val publicChatTitle = publicChatProfile?.title?.takeIf { it.isNotBlank() }
|
||||||
|
?: publicChatFallbackTitle.takeIf { activeInstanceId.isNotBlank() }
|
||||||
val publicChatLink = publicChatProfile?.let { "https://fromchat.ru/chats/${it.id}" }
|
val publicChatLink = publicChatProfile?.let { "https://fromchat.ru/chats/${it.id}" }
|
||||||
val deleteConfirmTitle = stringResource(Res.string.chat_delete_confirm_title)
|
val deleteConfirmTitle = stringResource(Res.string.chat_delete_confirm_title)
|
||||||
val deleteConfirmBody = stringResource(Res.string.chat_delete_confirm_body)
|
val deleteConfirmBody = stringResource(Res.string.chat_delete_confirm_body)
|
||||||
|
|||||||
+17
@@ -38,3 +38,20 @@ actual suspend fun wipeAttachmentCacheDirectories() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual suspend fun wipeInstanceAuxiliaryCacheDirectory(instanceId: String) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
val safe = instanceId.trim().replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
if (safe.isEmpty()) return@withContext
|
||||||
|
val url = NSFileManager.defaultManager.URLForDirectory(
|
||||||
|
directory = NSCachesDirectory,
|
||||||
|
inDomain = NSUserDomainMask,
|
||||||
|
appropriateForURL = null,
|
||||||
|
create = false,
|
||||||
|
error = null,
|
||||||
|
) ?: return@withContext
|
||||||
|
val base = url.path ?: return@withContext
|
||||||
|
NSFileManager.defaultManager.removeItemAtPath("$base/fromchat/instances/$safe", error = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user