Skip to content

Commit a5255ca

Browse files
authored
Merge pull request #9895 from shamim-emon/fix-issue-9822
feat(message-list): add Avatar to MessageListContent
2 parents c1f4fe8 + eb32b47 commit a5255ca

10 files changed

Lines changed: 217 additions & 14 deletions

File tree

core/android/common/src/main/kotlin/app/k9mail/core/android/common/contact/ContactRepository.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package app.k9mail.core.android.common.contact
22

3+
import android.net.Uri
34
import net.thunderbird.core.common.cache.Cache
45
import net.thunderbird.core.common.mail.EmailAddress
6+
import net.thunderbird.core.common.mail.toEmailAddressOrNull
57

68
interface ContactRepository {
79

@@ -10,6 +12,8 @@ interface ContactRepository {
1012
fun hasContactFor(emailAddress: EmailAddress): Boolean
1113

1214
fun hasAnyContactFor(emailAddresses: List<EmailAddress>): Boolean
15+
16+
fun getPhotoUri(emailAddress: String): Uri?
1317
}
1418

1519
interface CachingRepository {
@@ -42,6 +46,12 @@ internal class CachingContactRepository(
4246
override fun hasAnyContactFor(emailAddresses: List<EmailAddress>): Boolean =
4347
emailAddresses.any { emailAddress -> hasContactFor(emailAddress) }
4448

49+
override fun getPhotoUri(emailAddress: String): Uri? {
50+
return emailAddress.toEmailAddressOrNull()?.let { emailAddress ->
51+
getContactFor(emailAddress)?.photoUri
52+
}
53+
}
54+
4555
override fun clearCache() {
4656
cache.clear()
4757
}

core/android/common/src/test/kotlin/app/k9mail/core/android/common/contact/CachingContactRepositoryTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package app.k9mail.core.android.common.contact
22

3+
import android.net.Uri
34
import assertk.assertThat
45
import assertk.assertions.isEqualTo
56
import assertk.assertions.isFalse
@@ -140,4 +141,53 @@ internal class CachingContactRepositoryTest {
140141

141142
assertThat(cache[CONTACT_EMAIL_ADDRESS]).isNull()
142143
}
144+
145+
@Test
146+
fun `getPhotoUri() returns null when email is invalid`() {
147+
val result = testSubject.getPhotoUri("invalid-email")
148+
149+
assertThat(result).isNull()
150+
}
151+
152+
@Test
153+
fun `getPhotoUri() returns null when no contact found for valid email`() {
154+
dataSource.stub { on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturn null }
155+
156+
val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address)
157+
158+
assertThat(result).isNull()
159+
}
160+
161+
@Test
162+
fun `getPhotoUri() returns contact photo uri when contact exists`() {
163+
dataSource.stub { on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturn CONTACT }
164+
165+
val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address)
166+
167+
assertThat(result).isEqualTo(CONTACT.photoUri)
168+
}
169+
170+
@Test
171+
fun `getPhotoUri() returns cached photo uri when contact already cached`() {
172+
cache[CONTACT_EMAIL_ADDRESS] = CONTACT
173+
174+
val result = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address)
175+
176+
assertThat(result).isEqualTo(CONTACT.photoUri)
177+
}
178+
179+
@Test
180+
fun `getPhotoUri() caches result after first fetch`() {
181+
dataSource.stub {
182+
on { getContactFor(CONTACT_EMAIL_ADDRESS) } doReturnConsecutively listOf(
183+
CONTACT,
184+
CONTACT.copy(photoUri = Uri.parse("content://other/photo")),
185+
)
186+
}
187+
188+
val result1 = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address)
189+
val result2 = testSubject.getPhotoUri(CONTACT_EMAIL_ADDRESS.address)
190+
191+
assertThat(result1).isEqualTo(result2)
192+
}
143193
}

legacy/ui/legacy/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ dependencies {
2727
implementation(projects.feature.notification.api)
2828
// TODO: Remove AccountOauth dependency
2929
implementation(projects.feature.account.oauth)
30+
implementation(projects.feature.account.avatar.api)
31+
implementation(projects.feature.account.avatar.impl)
3032
implementation(projects.feature.funding.api)
3133
implementation(projects.feature.search.implLegacy)
3234
implementation(projects.feature.settings.import)

legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/messagelist/item/MessageItemContentPreview.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.fsck.k9.ui.messagelist.item
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.tooling.preview.PreviewLightDark
5+
import app.k9mail.core.android.common.contact.Contact
6+
import app.k9mail.core.android.common.contact.ContactRepository
57
import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark
68
import com.fsck.k9.FontSizes
79
import com.fsck.k9.UiDensity
@@ -12,7 +14,9 @@ import com.fsck.k9.ui.messagelist.MessageListAppearance
1214
import com.fsck.k9.ui.messagelist.MessageListItem
1315
import net.thunderbird.core.android.account.Identity
1416
import net.thunderbird.core.android.account.LegacyAccount
17+
import net.thunderbird.core.common.mail.EmailAddress
1518
import net.thunderbird.feature.account.AccountIdFactory
19+
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
1620
import net.thunderbird.feature.account.storage.profile.AvatarDto
1721
import net.thunderbird.feature.account.storage.profile.AvatarTypeDto
1822
import net.thunderbird.feature.account.storage.profile.ProfileDto
@@ -25,6 +29,8 @@ internal fun MessageItemContentPreview() {
2529
item = fakeMessageListItem,
2630
isActive = true,
2731
isSelected = false,
32+
contactRepository = fakeContactRepository,
33+
avatarMonogramCreator = fakeAvatarMonogramCreator,
2834
onClick = {},
2935
onLongClick = {},
3036
onAvatarClick = {},
@@ -97,3 +103,23 @@ private val fakeMessageListAppearance = MessageListAppearance(
97103
showAccountIndicator = true,
98104
density = UiDensity.Default,
99105
)
106+
107+
private val fakeContactRepository = object : ContactRepository {
108+
override fun getContactFor(emailAddress: EmailAddress): Contact? {
109+
error("Not implemented")
110+
}
111+
112+
override fun hasContactFor(emailAddress: EmailAddress): Boolean {
113+
error("Not implemented")
114+
}
115+
116+
override fun hasAnyContactFor(emailAddresses: List<EmailAddress>): Boolean {
117+
error("Not implemented")
118+
}
119+
120+
override fun getPhotoUri(emailAddress: String) = null
121+
}
122+
123+
private val fakeAvatarMonogramCreator = object : AvatarMonogramCreator {
124+
override fun create(name: String?, email: String?) = "SE"
125+
}

legacy/ui/legacy/src/main/java/com/fsck/k9/contacts/ContactPhotoLoader.kt

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ package com.fsck.k9.contacts
33
import android.content.ContentResolver
44
import android.graphics.Bitmap
55
import android.graphics.BitmapFactory
6-
import android.net.Uri
76
import app.k9mail.core.android.common.contact.ContactRepository
8-
import net.thunderbird.core.common.mail.toEmailAddressOrNull
97
import net.thunderbird.core.logging.legacy.Log
108

119
internal class ContactPhotoLoader(
1210
private val contentResolver: ContentResolver,
1311
private val contactRepository: ContactRepository,
1412
) {
1513
fun loadContactPhoto(emailAddress: String): Bitmap? {
16-
val photoUri = getPhotoUri(emailAddress) ?: return null
14+
val photoUri = contactRepository.getPhotoUri(emailAddress = emailAddress) ?: return null
1715
return try {
1816
contentResolver.openInputStream(photoUri).use { inputStream ->
1917
BitmapFactory.decodeStream(inputStream)
@@ -23,10 +21,4 @@ internal class ContactPhotoLoader(
2321
null
2422
}
2523
}
26-
27-
private fun getPhotoUri(email: String): Uri? {
28-
return email.toEmailAddressOrNull()?.let { emailAddress ->
29-
contactRepository.getContactFor(emailAddress)?.photoUri
30-
}
31-
}
3224
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.view.ViewGroup
1111
import androidx.compose.ui.platform.ComposeView
1212
import androidx.recyclerview.widget.DiffUtil
1313
import androidx.recyclerview.widget.RecyclerView
14+
import app.k9mail.core.android.common.contact.ContactRepository
1415
import app.k9mail.feature.launcher.FeatureLauncherActivity
1516
import app.k9mail.feature.launcher.FeatureLauncherTarget
1617
import app.k9mail.legacy.message.controller.MessageReference
@@ -27,6 +28,7 @@ import net.thunderbird.core.featureflag.FeatureFlagKey
2728
import net.thunderbird.core.featureflag.FeatureFlagProvider
2829
import net.thunderbird.core.featureflag.FeatureFlagResult
2930
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
31+
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
3032
import net.thunderbird.feature.notification.api.ui.action.NotificationAction
3133

3234
private const val FOOTER_ID = 1L
@@ -47,6 +49,8 @@ class MessageListAdapter internal constructor(
4749
private val relativeDateTimeFormatter: RelativeDateTimeFormatter,
4850
private val themeProvider: FeatureThemeProvider,
4951
private val featureFlagProvider: FeatureFlagProvider,
52+
private val contactRepository: ContactRepository,
53+
private val avatarMonogramCreator: AvatarMonogramCreator,
5054
) : RecyclerView.Adapter<MessageListViewHolder>() {
5155

5256
val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme)
@@ -266,6 +270,8 @@ class MessageListAdapter internal constructor(
266270
ComposableMessageViewHolder.create(
267271
context = parent.context,
268272
themeProvider = themeProvider,
273+
contactRepository = contactRepository,
274+
avatarMonogramCreator = avatarMonogramCreator,
269275
onClick = { listItemListener.onMessageClicked(it) },
270276
onLongClick = { listItemListener.onToggleMessageSelection(it) },
271277
onFavouriteClick = { listItemListener.onToggleMessageFlag(it) },

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.lifecycle.Observer
3434
import androidx.lifecycle.lifecycleScope
3535
import androidx.recyclerview.widget.RecyclerView
3636
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
37+
import app.k9mail.core.android.common.contact.ContactRepository
3738
import app.k9mail.legacy.message.controller.MessageReference
3839
import app.k9mail.legacy.message.controller.MessagingControllerRegistry
3940
import app.k9mail.legacy.message.controller.SimpleMessagingListener
@@ -92,6 +93,7 @@ import net.thunderbird.core.logging.Logger
9293
import net.thunderbird.core.logging.legacy.Log
9394
import net.thunderbird.core.preference.GeneralSettingsManager
9495
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
96+
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
9597
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
9698
import net.thunderbird.feature.mail.message.list.domain.DomainContract
9799
import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory
@@ -145,6 +147,9 @@ class MessageListFragment :
145147
private val activityListener = MessageListActivityListener()
146148
private val actionModeCallback = ActionModeCallback()
147149

150+
private val contactRepository: ContactRepository by inject()
151+
private val avatarMonogramCreator: AvatarMonogramCreator by inject()
152+
148153
private val chooseFolderForMoveLauncher: ActivityResultLauncher<ChooseFolderResultContract.Input> =
149154
registerForActivityResult(ChooseFolderResultContract(ChooseFolderActivity.Action.MOVE)) { result ->
150155
handleChooseFolderResult(result) { folderId, messages ->
@@ -348,6 +353,8 @@ class MessageListFragment :
348353
relativeDateTimeFormatter = RelativeDateTimeFormatter(requireContext(), clock),
349354
themeProvider = featureThemeProvider,
350355
featureFlagProvider = featureFlagProvider,
356+
contactRepository = contactRepository,
357+
avatarMonogramCreator = avatarMonogramCreator,
351358
).apply {
352359
activeMessage = this@MessageListFragment.activeMessage
353360
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/item/ComposableMessageViewHolder.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package com.fsck.k9.ui.messagelist.item
22

33
import android.content.Context
44
import androidx.compose.ui.platform.ComposeView
5+
import app.k9mail.core.android.common.contact.ContactRepository
56
import com.fsck.k9.ui.messagelist.MessageListAppearance
67
import com.fsck.k9.ui.messagelist.MessageListItem
78
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
9+
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
810

911
/**
1012
* A composable view holder for message list items.
1113
*/
14+
@Suppress("LongParameterList")
1215
class ComposableMessageViewHolder(
1316
private val composeView: ComposeView,
1417
private val themeProvider: FeatureThemeProvider,
@@ -17,6 +20,8 @@ class ComposableMessageViewHolder(
1720
private val onAvatarClick: (MessageListItem) -> Unit,
1821
private val onFavouriteClick: (MessageListItem) -> Unit,
1922
private val appearance: MessageListAppearance,
23+
private val contactRepository: ContactRepository,
24+
private val avatarMonogramCreator: AvatarMonogramCreator,
2025
) : MessageListViewHolder(composeView) {
2126

2227
var uniqueId: Long = -1L
@@ -30,6 +35,8 @@ class ComposableMessageViewHolder(
3035
item = item,
3136
isActive = isActive,
3237
isSelected = isSelected,
38+
contactRepository = contactRepository,
39+
avatarMonogramCreator = avatarMonogramCreator,
3340
onClick = { onClick(item) },
3441
onLongClick = { onLongClick(item) },
3542
onAvatarClick = { onAvatarClick(item) },
@@ -41,10 +48,12 @@ class ComposableMessageViewHolder(
4148
}
4249

4350
companion object {
44-
51+
@Suppress("LongParameterList")
4552
fun create(
4653
context: Context,
4754
themeProvider: FeatureThemeProvider,
55+
contactRepository: ContactRepository,
56+
avatarMonogramCreator: AvatarMonogramCreator,
4857
onClick: (MessageListItem) -> Unit,
4958
onLongClick: (MessageListItem) -> Unit,
5059
onFavouriteClick: (MessageListItem) -> Unit,
@@ -56,6 +65,8 @@ class ComposableMessageViewHolder(
5665
val holder = ComposableMessageViewHolder(
5766
composeView = composeView,
5867
themeProvider = themeProvider,
68+
contactRepository = contactRepository,
69+
avatarMonogramCreator = avatarMonogramCreator,
5970
onClick = onClick,
6071
onLongClick = onLongClick,
6172
onAvatarClick = onAvatarClick,

0 commit comments

Comments
 (0)