diff --git a/homebase-api/src/commonMain/kotlin/id/homebase/api/client/auth/ApiCredentials.kt b/homebase-api/src/commonMain/kotlin/id/homebase/api/client/auth/ApiCredentials.kt index f1f1ab08..b483eebd 100644 --- a/homebase-api/src/commonMain/kotlin/id/homebase/api/client/auth/ApiCredentials.kt +++ b/homebase-api/src/commonMain/kotlin/id/homebase/api/client/auth/ApiCredentials.kt @@ -19,7 +19,8 @@ data class ApiCredentials private constructor( fun getIdentityId(): Uuid { // TODO: <- get the real identityId - val identityId = Uuid.parse("7b1be23b-48bb-4304-bc7b-db5910c09a92") +// val identityId = Uuid.parse("7b1be23b-48bb-4304-bc7b-db5910c09a92") + val identityId = this.domain.toHashId() return identityId } diff --git a/homebase-api/src/commonMain/kotlin/id/homebase/api/client/drives/cache/DriveFileProviderCached.kt b/homebase-api/src/commonMain/kotlin/id/homebase/api/client/drives/cache/DriveFileProviderCached.kt index 4aae6015..6784f072 100644 --- a/homebase-api/src/commonMain/kotlin/id/homebase/api/client/drives/cache/DriveFileProviderCached.kt +++ b/homebase-api/src/commonMain/kotlin/id/homebase/api/client/drives/cache/DriveFileProviderCached.kt @@ -53,10 +53,21 @@ class DriveFileProviderCached( @Volatile private var notFoundCache: Set = emptySet() private val notFoundCacheMutex = Mutex() + // All cache directories are scoped under the active user's hashed domain so that + // switching accounts never reads another user's cached data. + private val userDirectory: String by lazy { + kotlinx.coroutines.runBlocking { + "$directory/${credentialsManager.requireActiveDomain().toHashId()}" + } + } + private val payloadsDirectory get() = "$userDirectory/homebase-payloads" + private val thumbsDirectory get() = "$userDirectory/homebase-thumbs" + private val preloadDirectory get() = "$userDirectory/hbvid_preload" + private val payloadDiskKache by lazy { kotlinx.coroutines.runBlocking { FileKache( - directory = "$directory/homebase-payloads", + directory = payloadsDirectory, maxSize = 200L * 1024L * 1024L // 200MB ) } @@ -65,7 +76,7 @@ class DriveFileProviderCached( private val thumbDiskKache by lazy { kotlinx.coroutines.runBlocking { FileKache( - directory = "$directory/homebase-thumbs", + directory = thumbsDirectory, maxSize = 300L * 1024L * 1024L // 300MB ) } @@ -412,22 +423,18 @@ class DriveFileProviderCached( .joinToString(":") suspend fun clearCaches() { - val payloadDir = "$directory/homebase-payloads".toPath() - val thumbDir = "$directory/homebase-thumbs".toPath() - val preloadDir = "$directory/hbvid_preload".toPath() - try { payloadDiskKache.clear() thumbDiskKache.clear() } catch (e: Exception) { Logger.w("Kache.clear() failed, falling back to manual delete", e) - fileSystem.delete(payloadDir, mustExist = false) - fileSystem.delete(thumbDir, mustExist = false) + fileSystem.delete(payloadsDirectory.toPath(), mustExist = false) + fileSystem.delete(thumbsDirectory.toPath(), mustExist = false) } try { - fileSystem.deleteRecursively(preloadDir) + fileSystem.deleteRecursively(preloadDirectory.toPath()) } catch (_: Exception) {} notFoundCache = emptySet() diff --git a/homebase-api/src/commonMain/kotlin/id/homebase/api/sync/database/ChatReadCountWrapper.kt b/homebase-api/src/commonMain/kotlin/id/homebase/api/sync/database/ChatReadCountWrapper.kt index eea4705a..90766acc 100644 --- a/homebase-api/src/commonMain/kotlin/id/homebase/api/sync/database/ChatReadCountWrapper.kt +++ b/homebase-api/src/commonMain/kotlin/id/homebase/api/sync/database/ChatReadCountWrapper.kt @@ -39,8 +39,8 @@ class ChatReadCountWrapper( * Select all conversations (fileType 8888) from DriveMainIndex * Note: This implementation is simplified and would need the generated SQLDelight queries */ - fun selectAllConversations(): List { - val list = delegate.selectAllCoversations().executeAsList() + fun selectAllConversations(identityId: Uuid): List { + val list = delegate.selectAllCoversations(identityId).executeAsList() return list.map { OdinSystemSerializer.deserialize(it) } } @@ -52,11 +52,11 @@ class ChatReadCountWrapper( * Note: This implementation is simplified and would need the generated SQLDelight queries */ - fun selectAllConversationPlusLastMessage(): List { + fun selectAllConversationPlusLastMessage(identityId: Uuid): List { val start = Clock.System.now().toEpochMilliseconds() - val list = delegate.selectAllConversationPlusLastMessage().executeAsList() + val list = delegate.selectAllConversationPlusLastMessage(identityId).executeAsList() logger.d { "Fetched rows=${list.size} in ${Clock.System.now().toEpochMilliseconds() - start}ms" } @@ -118,8 +118,8 @@ class ChatReadCountWrapper( * Get all conversation read counts * Note: This implementation is simplified and would need the generated SQLDelight queries */ - suspend fun selectAllUnreadCount(originalAuthor: OdinId): List { - val list = delegate.selectAllUnreadCount(originalAuthor.domainName).executeAsList() + suspend fun selectAllUnreadCount(identityId: Uuid, originalAuthor: OdinId): List { + val list = delegate.selectAllUnreadCount(identityId, originalAuthor.domainName).executeAsList() return list.map { ConversationUnreadCount( conversationId = it.groupId, diff --git a/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/ChatReadCount.sq b/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/ChatReadCount.sq index 3ee396e2..272ef152 100644 --- a/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/ChatReadCount.sq +++ b/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/ChatReadCount.sq @@ -22,7 +22,7 @@ CREATE INDEX IF NOT EXISTS idx_messages_filetype_datatype_groupid_created selectAllCoversations: SELECT jsonHeader FROM DriveMainIndex -WHERE fileType = 8888; +WHERE identityId = ? AND fileType = 8888; -- Like above, but add last message from each conversation -- This is needed at boot-time to efficiently load the full list of conversations @@ -38,13 +38,14 @@ LEFT JOIN DriveMainIndex AS m ON m.rowId = ( SELECT m2.rowId FROM DriveMainIndex AS m2 - WHERE m2.groupId = d.uniqueId + WHERE m2.identityId = d.identityId + AND m2.groupId = d.uniqueId AND m2.fileType = 7878 AND m2.dataType = 0 ORDER BY m2.created DESC LIMIT 1 ) -WHERE d.fileType = 8888 -- Conversation +WHERE d.identityId = ? AND d.fileType = 8888 -- Conversation ORDER BY msgCreated DESC; -- Return unread message count for a specific groupId @@ -73,7 +74,8 @@ selectAllUnreadCount: SELECT d.groupId, COUNT(*) AS unreadCount FROM DriveMainIndex AS d LEFT JOIN ChatReadCount c ON d.groupId = c.groupId -WHERE d.fileType = 7878 -- Message +WHERE d.identityId = ? + AND d.fileType = 7878 -- Message AND d.dataType = 0 -- Standard message AND (c.lastReadTime IS NULL OR d.created > c.lastReadTime) AND d.groupId IS NOT NULL diff --git a/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/DriveMainIndex.sq b/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/DriveMainIndex.sq index db46a2c7..1f3c7d03 100644 --- a/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/DriveMainIndex.sq +++ b/homebase-api/src/commonMain/sqldelight/id/homebase/api/sync/database/DriveMainIndex.sq @@ -1,6 +1,6 @@ import kotlin.uuid.Uuid; -CREATE TABLE IF NOT EXISTS DriveMainIndex( -- Version: 2 +CREATE TABLE IF NOT EXISTS DriveMainIndex( -- Version: 3 rowId INTEGER PRIMARY KEY AUTOINCREMENT, identityId BLOB AS Uuid NOT NULL, driveId BLOB AS Uuid NOT NULL, diff --git a/homebase-api/src/commonTest/kotlin/id/homebase/api/sync/database/ChatReadCountWrapperTest.kt b/homebase-api/src/commonTest/kotlin/id/homebase/api/sync/database/ChatReadCountWrapperTest.kt index 2bad8043..09087b12 100644 --- a/homebase-api/src/commonTest/kotlin/id/homebase/api/sync/database/ChatReadCountWrapperTest.kt +++ b/homebase-api/src/commonTest/kotlin/id/homebase/api/sync/database/ChatReadCountWrapperTest.kt @@ -163,7 +163,7 @@ class ChatReadCountWrapperTest { val testData = populateMockData(dbm) val wrapper = dbm.chatReadCount - val conversations = wrapper.selectAllConversations() + val conversations = wrapper.selectAllConversations(testData.identityId) // Should return all 3 conversations (fileType 8888) assertEquals(3, conversations.size, "Should return all conversations") @@ -187,7 +187,7 @@ class ChatReadCountWrapperTest { DatabaseManager { createInMemoryDatabase() }.use { dbm -> val wrapper = dbm.chatReadCount - val conversations = wrapper.selectAllConversations() + val conversations = wrapper.selectAllConversations(Uuid.random()) assertTrue( conversations.isEmpty(), "Should return empty list when no conversations exist" @@ -201,7 +201,7 @@ class ChatReadCountWrapperTest { val testData = populateMockData(dbm) val wrapper = dbm.chatReadCount - val conversationsWithMessages = wrapper.selectAllConversationPlusLastMessage() + val conversationsWithMessages = wrapper.selectAllConversationPlusLastMessage(testData.identityId) // Should return all 3 conversations assertEquals( @@ -261,7 +261,7 @@ class ChatReadCountWrapperTest { DatabaseManager { createInMemoryDatabase() }.use { dbm -> val wrapper = dbm.chatReadCount - val conversationsWithMessages = wrapper.selectAllConversationPlusLastMessage() + val conversationsWithMessages = wrapper.selectAllConversationPlusLastMessage(Uuid.random()) assertTrue( conversationsWithMessages.isEmpty(), @@ -415,7 +415,7 @@ class ChatReadCountWrapperTest { val wrapper = dbm.chatReadCount val originalAuthor: OdinId = OdinId("somewhere.demo.rocks") - val allReadCounts = wrapper.selectAllUnreadCount(originalAuthor) + val allReadCounts = wrapper.selectAllUnreadCount(Uuid.random(), originalAuthor) assertTrue( allReadCounts.isEmpty(), "Should return empty list when no conversations exist" diff --git a/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationService.kt b/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationService.kt index aabd3457..92a9022f 100644 --- a/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationService.kt +++ b/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationService.kt @@ -47,7 +47,6 @@ class ConversationService( private val credentialsManager: CredentialsManager, private val payloadBundleEncryptionService: PayloadBundleEncryptionService, private val dbm: DatabaseManager, - private val contactService: ContactService, private val introductionProvider: ConnectionIntroductionProvider, private val scope: CoroutineScope, private val outboxSync: OutboxSync, diff --git a/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationStream.kt b/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationStream.kt index b1ac7834..17e6087d 100644 --- a/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationStream.kt +++ b/homebase-chat/src/commonMain/kotlin/id/homebase/chat/services/convo/ConversationStream.kt @@ -307,8 +307,9 @@ class ConversationStream( } suspend fun updateUnreadCounts() { - val domain = credentialsManager.requireActiveDomain() - val unread = dbm.chatReadCount.selectAllUnreadCount(domain) + val c = credentialsManager.requireActiveCredentials() + val domain = c.domain + val unread = dbm.chatReadCount.selectAllUnreadCount(c.getIdentityId(), domain) val unreadMap = unread.associate { it.conversationId to it.unreadCount.toInt() } var changed = false @@ -329,10 +330,10 @@ class ConversationStream( } suspend fun fetchConversations(): List { - val result = dbm.chatReadCount.selectAllConversationPlusLastMessage() - val conversations = result.map { mapper.mapToConversationUi(it.conversation, it.message) } val c = credentialsManager.requireActiveCredentials() val domain = c.domain + val result = dbm.chatReadCount.selectAllConversationPlusLastMessage(c.getIdentityId()) + val conversations = result.map { mapper.mapToConversationUi(it.conversation, it.message) } var self = ChatProtocol.buildSelfConversation(domain) // Query the latest message for the self-conversation directly from the DB.