diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt index 487be14087..26bbc2eb0c 100644 --- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt +++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/UserProfileDao.kt @@ -27,8 +27,10 @@ interface UserProfileDao { * Uses `INSERT OR REPLACE` with correlated sub-selects (rather than `ON CONFLICT DO * UPDATE`) so it works on the minSdk-29 SQLite build, which predates UPSERT. The * sub-selects read the current row before the replace, preserving every column the - * blocklist doesn't know about; the avatar keeps its existing value when [profilePicture] - * is null via COALESCE. + * blocklist doesn't know about. The name and avatar keep their existing values when + * [displayName] or [profilePicture] is null, via COALESCE: null means the caller has no + * profile to write (a failed fetch), whereas an empty name is one the user really has. A + * null name with nothing cached stores an empty one, since the column is NOT NULL. */ @Query( """ @@ -38,7 +40,7 @@ interface UserProfileDao { profile_picture_json, username, pending_migration_json ) VALUES ( :userIdHex, - :displayName, + COALESCE(:displayName, (SELECT display_name FROM user_profiles WHERE user_id_hex = :userIdHex), ''), (SELECT phone_value FROM user_profiles WHERE user_id_hex = :userIdHex), (SELECT phone_verified FROM user_profiles WHERE user_id_hex = :userIdHex), (SELECT email_value FROM user_profiles WHERE user_id_hex = :userIdHex), @@ -50,7 +52,7 @@ interface UserProfileDao { ) """ ) - suspend fun upsertNameAndAvatar(userIdHex: String, displayName: String, profilePicture: MediaItem?) + suspend fun upsertNameAndAvatar(userIdHex: String, displayName: String?, profilePicture: MediaItem?) /** The cached profile for [userIdHex], or null if none is cached. */ @Query("SELECT * FROM user_profiles WHERE user_id_hex = :userIdHex LIMIT 1") diff --git a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/UserProfileMigrationTest.kt b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/UserProfileMigrationTest.kt index 7e876c8409..0504bcc6e2 100644 --- a/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/UserProfileMigrationTest.kt +++ b/apps/flipcash/shared/persistence/db/src/test/kotlin/com/flipcash/app/persistence/UserProfileMigrationTest.kt @@ -200,6 +200,37 @@ class UserProfileDaoTest { assertEquals("alice", dao.getByUserId("u1")?.username) } + @Test + fun `partial write with no name keeps the cached name`() = runBlocking { + val dao = db.userProfileDao() + dao.upsertFull(listOf(fullProfile("u1"))) + + // The blocklist writes no name when the profile fetch failed; that is not a rename. + dao.upsertNameAndAvatar(userIdHex = "u1", displayName = null, profilePicture = null) + + assertEquals("Alice", dao.getByUserId("u1")?.displayName) + } + + @Test + fun `partial write with no name and nothing cached stores an empty name`() = runBlocking { + val dao = db.userProfileDao() + + dao.upsertNameAndAvatar(userIdHex = "u1", displayName = null, profilePicture = null) + + assertEquals("", dao.getByUserId("u1")?.displayName) + } + + @Test + fun `partial write with an empty name replaces the cached name`() = runBlocking { + val dao = db.userProfileDao() + dao.upsertFull(listOf(fullProfile("u1"))) + + // A fetched profile with no name is the user's own choice, not a failed lookup. + dao.upsertNameAndAvatar(userIdHex = "u1", displayName = "", profilePicture = null) + + assertEquals("", dao.getByUserId("u1")?.displayName) + } + @Test fun `chat member relation joins the shared normalized profile`() = runBlocking { db.userProfileDao().upsertFull(listOf(fullProfile("u1"))) diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/BlockedUserDataSource.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/BlockedUserDataSource.kt index 20692a7440..7f0c9d07d2 100644 --- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/BlockedUserDataSource.kt +++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/BlockedUserDataSource.kt @@ -62,12 +62,13 @@ class BlockedUserDataSource @Inject constructor( /** * Writes the blocked user's name + avatar into the shared `user_profiles` table without * disturbing any richer profile already cached from a chat (the blocklist only resolves - * those two fields). See [com.flipcash.app.persistence.dao.UserProfileDao.upsertNameAndAvatar]. + * those two fields). A null profile means the fetch failed, so the cached name stays rather + * than being blanked. See [com.flipcash.app.persistence.dao.UserProfileDao.upsertNameAndAvatar]. */ private suspend fun FlipcashDatabase.writeProfile(resolved: ResolvedBlockedUser) { userProfileDao().upsertNameAndAvatar( userIdHex = resolved.blocked.userId.hexEncodedString(), - displayName = resolved.profile?.displayName.orEmpty(), + displayName = resolved.profile?.displayName, profilePicture = resolved.profile?.profilePicture, ) }