[SES-2512] Rewrite ProfilePictureView (#1622)

This commit is contained in:
Fanchao Liu 2024-08-19 17:32:30 +10:00 committed by GitHub
parent 6701cb1dc1
commit 9919f716a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 251 additions and 391 deletions

View File

@ -27,8 +27,8 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityWebrtcBinding
import org.apache.commons.lang3.time.DurationFormatUtils
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -194,13 +194,8 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
clipFloatingInsets()
// set up the user avatar
TextSecurePreferences.getLocalNumber(this)?.let{
val username = TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(it)
binding.userAvatar.apply {
publicKey = it
displayName = username
update()
}
TextSecurePreferences.getLocalNumber(this)?.let {
binding.userAvatar.load(Address.fromSerialized(it))
}
}
@ -332,8 +327,6 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
launch {
viewModel.recipient.collect { latestRecipient ->
binding.contactAvatar.recycle()
if (latestRecipient.recipient != null) {
val contactPublicKey = latestRecipient.recipient.address.serialize()
val contactDisplayName = getUserDisplayName(contactPublicKey)
@ -341,11 +334,7 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
binding.remoteRecipientName.text = contactDisplayName
// sort out the contact's avatar
binding.contactAvatar.apply {
publicKey = contactPublicKey
displayName = contactDisplayName
update()
}
binding.contactAvatar.load(latestRecipient.recipient)
}
}
}

View File

@ -3,164 +3,224 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.ContactPhoto
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.database.GroupDatabase
import javax.inject.Inject
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val TAG = "ProfilePictureView"
@AndroidEntryPoint
class ProfilePictureView : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
@Inject
lateinit var groupDatabase: GroupDatabase
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: RequestManager = Glide.with(this)
private val prefs = AppTextSecurePreferences(context)
private val userPublicKey = prefs.getLocalNumber()
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
var additionalDisplayName: String? = null
private var lastLoadJob: Job? = null
private var lastLoadAddress: Address? = null
private val profilePicturesCache = mutableMapOf<View, Recipient>()
private val resourcePadding by lazy {
context.resources.getDimensionPixelSize(R.dimen.normal_padding).toFloat()
}
private val unknownRecipientDrawable by lazy { ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false, resourcePadding) }
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false, resourcePadding) }
constructor(context: Context, sender: Recipient): this(context) {
update(sender)
private val unknownRecipientDrawable by lazy(LazyThreadSafetyMode.NONE) {
ResourceContactPhoto(R.drawable.ic_profile_default)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
}
fun update(recipient: Recipient) {
recipient.run { update(address, isClosedGroupRecipient, isOpenGroupInboxRecipient) }
private val unknownOpenGroupDrawable by lazy(LazyThreadSafetyMode.NONE) {
ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
}
fun update(
address: Address,
isClosedGroupRecipient: Boolean = false,
isOpenGroupInboxRecipient: Boolean = false
) {
fun getUserDisplayName(publicKey: String): String = prefs.takeIf { userPublicKey == publicKey }?.getProfileName()
?: DatabaseComponent.get(context).sessionContactDatabase().getContactWithAccountID(publicKey)?.displayName(Contact.ContactContext.REGULAR)
?: publicKey
private fun setShowAsDoubleMode(showAsDouble: Boolean) {
binding.doubleModeImageViewContainer.isVisible = showAsDouble
binding.singleModeImageView.isVisible = !showAsDouble
}
if (isClosedGroupRecipient) {
val members = DatabaseComponent.get(context).groupDatabase()
.getGroupMemberAddresses(address.toGroupString(), true)
.sorted()
.take(2)
if (members.size <= 1) {
publicKey = ""
displayName = ""
additionalPublicKey = ""
additionalDisplayName = ""
} else {
val pk = members.getOrNull(0)?.serialize() ?: ""
publicKey = pk
displayName = getUserDisplayName(pk)
val apk = members.getOrNull(1)?.serialize() ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
private fun cancelLastLoadJob() {
lastLoadJob?.cancel()
lastLoadJob = null
}
@OptIn(DelicateCoroutinesApi::class)
private fun loadAsDoubleImages(model: LoadModel) {
cancelLastLoadJob()
// The use of GlobalScope is intentional here, as there is no better lifecycle scope that we can use
// to launch a coroutine from a view. The potential memory leak is not a concern here, as
// the coroutine is very short-lived. If you change the code here to be long live then you'll
// need to find a better scope to launch the coroutine from.
lastLoadJob = GlobalScope.launch(Dispatchers.Main) {
data class GroupMemberInfo(
val contactPhoto: ContactPhoto?,
val placeholderAvatarPhoto: PlaceholderAvatarPhoto,
)
// Load group avatar if available, otherwise load member avatars
val groupAvatarOrMemberAvatars = withContext(Dispatchers.Default) {
model.loadRecipient(context).contactPhoto
?: groupDatabase.getGroupMembers(model.address.toGroupString(), true)
.map {
GroupMemberInfo(
contactPhoto = it.contactPhoto,
placeholderAvatarPhoto = PlaceholderAvatarPhoto(
hashString = it.address.serialize(),
displayName = it.displayName()
)
)
}
}
} else if(isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInboxAccountId(address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
} else {
val publicKey = address.serialize()
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
}
update()
}
fun update() {
val publicKey = publicKey ?: return Log.w(TAG, "Could not find public key to update profile picture")
val additionalPublicKey = additionalPublicKey
// if we have a multi avatar setup
if (additionalPublicKey != null) {
setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName)
setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName)
binding.doubleModeImageViewContainer.visibility = View.VISIBLE
when (groupAvatarOrMemberAvatars) {
is ContactPhoto -> {
setShowAsDoubleMode(false)
Glide.with(this@ProfilePictureView)
.load(groupAvatarOrMemberAvatars)
.error(unknownRecipientDrawable)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(binding.singleModeImageView)
}
// clear single image
glide.clear(binding.singleModeImageView)
binding.singleModeImageView.visibility = View.INVISIBLE
} else { // single image mode
setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName)
binding.singleModeImageView.visibility = View.VISIBLE
is List<*> -> {
val first = groupAvatarOrMemberAvatars.getOrNull(0) as? GroupMemberInfo
val second = groupAvatarOrMemberAvatars.getOrNull(1) as? GroupMemberInfo
setShowAsDoubleMode(true)
Glide.with(binding.doubleModeImageView1)
.load(first?.let { it.contactPhoto ?: it.placeholderAvatarPhoto })
.error(first?.placeholderAvatarPhoto ?: unknownRecipientDrawable)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(binding.doubleModeImageView1)
// clear multi image
glide.clear(binding.doubleModeImageView1)
glide.clear(binding.doubleModeImageView2)
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
}
Glide.with(binding.doubleModeImageView2)
.load(second?.let { it.contactPhoto ?: it.placeholderAvatarPhoto })
.error(second?.placeholderAvatarPhoto ?: unknownRecipientDrawable)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(binding.doubleModeImageView2)
}
}
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?) {
if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
if (profilePicturesCache[imageView] == recipient) return
profilePicturesCache[imageView] = recipient
val signalProfilePicture = recipient.contactPhoto
val avatar = (signalProfilePicture as? ProfileContactPhoto)?.avatarObject
glide.clear(imageView)
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
glide.load(signalProfilePicture)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.error(glide.load(placeholder))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imageView)
} else if (recipient.isCommunityRecipient && recipient.groupAvatarId == null) {
glide.clear(imageView)
glide.load(unknownOpenGroupDrawable)
.centerCrop()
.circleCrop()
.into(imageView)
} else {
glide.load(placeholder)
.placeholder(unknownRecipientDrawable)
.centerCrop()
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE).circleCrop().into(imageView)
else -> {
setShowAsDoubleMode(false)
binding.singleModeImageView.setImageDrawable(unknownRecipientDrawable)
}
}
} else {
glide.load(unknownRecipientDrawable)
.centerCrop()
.into(imageView)
}
}
fun recycle() {
profilePicturesCache.clear()
@OptIn(DelicateCoroutinesApi::class)
private fun loadAsSingleImage(model: LoadModel) {
cancelLastLoadJob()
setShowAsDoubleMode(false)
// Only clear the old image if the address has changed. This is important as we have a delay
// in loading the image, if this view is reused for another address before the image is loaded,
// the previous image could be displayed for a short period of time. We would want to avoid
// displaying the wrong image, even for a short time.
// However, if we are displaying the same user's image again, it's ok to show the old
// image until the new one is loaded. This is a trade-off between performance and correctness.
if (lastLoadAddress != model.address) {
Glide.with(this).clear(this)
}
// The use of GlobalScope is intentional here, as there is no better lifecycle scope that we can use
// to launch a coroutine from a view. The potential memory leak is not a concern here, as
// the coroutine is very short-lived. If you change the code here to be long live then you'll
// need to find a better scope to launch the coroutine from.
lastLoadJob = GlobalScope.launch(Dispatchers.Main) {
val (contactPhoto, avatarPlaceholder) = withContext(Dispatchers.Default) {
model.loadRecipient(context).let {
it.contactPhoto to PlaceholderAvatarPhoto(it.address.serialize(), it.displayName())
}
}
val address = model.address
val errorModel: Any = when {
address.isCommunity -> unknownOpenGroupDrawable
address.isContact -> avatarPlaceholder
else -> unknownRecipientDrawable
}
Glide.with(this@ProfilePictureView)
.load(contactPhoto)
.error(errorModel)
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(binding.singleModeImageView)
}
}
fun load(recipient: Recipient) {
if (recipient.address.isClosedGroup) {
loadAsDoubleImages(LoadModel.RecipientModel(recipient))
} else {
loadAsSingleImage(LoadModel.RecipientModel(recipient))
}
lastLoadAddress = recipient.address
}
fun load(address: Address) {
if (address.isClosedGroup) {
loadAsDoubleImages(LoadModel.AddressModel(address))
} else {
loadAsSingleImage(LoadModel.AddressModel(address))
}
lastLoadAddress = address
}
private fun Recipient.displayName(): String {
return if (isLocalNumber) {
TextSecurePreferences.getProfileName(context).orEmpty()
} else {
profileName ?: name ?: ""
}
}
private sealed interface LoadModel {
val address: Address
/**
* Load the recipient if it's not already loaded.
*/
fun loadRecipient(context: Context): Recipient
data class AddressModel(override val address: Address) : LoadModel {
override fun loadRecipient(context: Context): Recipient {
return Recipient.from(context, address, false)
}
}
data class RecipientModel(val recipient: Recipient) : LoadModel {
override val address: Address
get() = recipient.address
override fun loadRecipient(context: Context): Recipient = recipient
}
}
// endregion
}

View File

@ -53,7 +53,7 @@ class UserView : LinearLayout {
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
val address = user.address.serialize()
binding.profilePictureView.update(user)
binding.profilePictureView.load(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@ -85,7 +85,6 @@ class UserView : LinearLayout {
}
fun unbind() {
binding.profilePictureView.recycle()
}
// endregion
}

View File

@ -81,7 +81,7 @@ class ConversationActionBarView @JvmOverloads constructor(
}
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
binding.profilePictureView.update(recipient)
binding.profilePictureView.load(recipient)
binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.note_to_self)
updateSubtitle(recipient, openGroup, config)

View File

@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.database.getLong
import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
import org.thoughtcrime.securesms.showSessionDialog
import java.util.concurrent.atomic.AtomicLong

View File

@ -1,87 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import org.session.libsession.messaging.mentions.Mention
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.toPx
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
private var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
var glide: RequestManager? = null
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
var openGroupServer: String? = null
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupServer = openGroupServer }
var openGroupRoom: String? = null
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.openGroupRoom = openGroupRoom }
var onMentionCandidateSelected: ((Mention) -> Unit)? = null
private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) }
private class Adapter(private val context: Context) : BaseAdapter() {
var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; notifyDataSetChanged() }
var glide: RequestManager? = null
var openGroupServer: String? = null
var openGroupRoom: String? = null
override fun getCount(): Int {
return mentionCandidates.count()
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItem(position: Int): Mention {
return mentionCandidates[position]
}
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context)
val mentionCandidate = getItem(position)
cell.glide = glide
cell.mentionCandidate = mentionCandidate
cell.openGroupServer = openGroupServer
cell.openGroupRoom = openGroupRoom
return cell
}
}
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null)
init {
clipToOutline = true
adapter = mentionCandidateSelectionViewAdapter
mentionCandidateSelectionViewAdapter.mentionCandidates = mentionCandidates
setOnItemClickListener { _, _, position, _ ->
onMentionCandidateSelected?.invoke(mentionCandidates[position])
}
}
fun show(mentionCandidates: List<Mention>, threadID: Long) {
val openGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID)
if (openGroup != null) {
openGroupServer = openGroup.server
openGroupRoom = openGroup.room
}
this.mentionCandidates = mentionCandidates
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
layoutParams.height = toPx(Math.min(mentionCandidates.count(), 4) * 44, resources)
this.layoutParams = layoutParams
}
fun hide() {
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
layoutParams.height = 0
this.layoutParams = layoutParams
}
}

View File

@ -1,42 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import network.loki.messenger.databinding.ViewMentionCandidateBinding
import org.session.libsession.messaging.mentions.Mention
import org.thoughtcrime.securesms.groups.OpenGroupManager
import com.bumptech.glide.RequestManager
class MentionCandidateView : LinearLayout {
private lateinit var binding: ViewMentionCandidateBinding
var mentionCandidate = Mention("", "")
set(newValue) { field = newValue; update() }
var glide: RequestManager? = null
var openGroupServer: String? = null
var openGroupRoom: String? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
private fun initialize() {
binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true)
}
private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName
profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.displayName = mentionCandidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else {
moderatorIconImageView.visibility = View.GONE
}
}
}

View File

@ -2,13 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions
import android.view.View
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
fun ViewMentionCandidateV2Binding.update(candidate: MentionViewModel.Candidate) {
mentionCandidateNameTextView.text = candidate.nameHighlighted
profilePictureView.publicKey = candidate.member.publicKey
profilePictureView.displayName = candidate.member.name
profilePictureView.additionalPublicKey = null
profilePictureView.update()
profilePictureView.load(Address.fromSerialized(candidate.member.publicKey))
moderatorIconImageView.visibility = if (candidate.member.isModerator) View.VISIBLE else View.GONE
}

View File

@ -16,7 +16,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
@ -26,6 +25,8 @@ import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
@ -52,8 +53,6 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toDp
@ -179,8 +178,7 @@ class VisibleMessageView : FrameLayout {
if (isGroupThread && !message.isOutgoing) {
if (isEndOfMessageCluster) {
binding.profilePictureView.publicKey = senderAccountID
binding.profilePictureView.update(message.individualRecipient)
binding.profilePictureView.load(message.individualRecipient)
binding.profilePictureView.setOnClickListener {
if (thread.isCommunityRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
@ -456,7 +454,6 @@ class VisibleMessageView : FrameLayout {
}
fun recycle() {
binding.profilePictureView.recycle()
binding.messageContentView.root.recycle()
}

View File

@ -13,14 +13,14 @@ import org.session.libsession.avatars.ContactPhoto;
import java.io.IOException;
import java.io.InputStream;
class ContactPhotoFetcher implements DataFetcher<InputStream> {
public class ContactPhotoFetcher implements DataFetcher<InputStream> {
private final Context context;
private final ContactPhoto contactPhoto;
private InputStream inputStream;
ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
public ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
this.context = context.getApplicationContext();
this.contactPhoto = contactPhoto;
}

View File

@ -9,12 +9,15 @@ import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator
class PlaceholderAvatarFetcher(private val context: Context,
private val photo: PlaceholderAvatarPhoto): DataFetcher<BitmapDrawable> {
class PlaceholderAvatarFetcher(
private val context: Context,
private val hashString: String,
private val displayName: String
): DataFetcher<BitmapDrawable> {
override fun loadData(priority: Priority,callback: DataFetcher.DataCallback<in BitmapDrawable>) {
try {
val avatar = AvatarPlaceholderGenerator.generate(context, 128, photo.hashString, photo.displayName)
val avatar = AvatarPlaceholderGenerator.generate(context, 128, hashString, displayName)
callback.onDataReady(avatar)
} catch (e: Exception) {
Log.e("Loki", "Error in fetching avatar")

View File

@ -8,6 +8,9 @@ import com.bumptech.glide.load.model.ModelLoader.LoadData
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import org.session.libsession.avatars.PlaceholderAvatarPhoto
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
@ -17,7 +20,15 @@ class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<Plac
height: Int,
options: Options
): LoadData<BitmapDrawable> {
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
val displayName: String = when {
!model.displayName.isNullOrBlank() -> model.displayName.orEmpty()
model.hashString == TextSecurePreferences.getLocalNumber(appContext) -> TextSecurePreferences.getProfileName(appContext).orEmpty()
else -> Recipient.from(appContext, Address.fromSerialized(model.hashString), false).let {
it.profileName ?: it.name ?: ""
}
}
return LoadData(model, PlaceholderAvatarFetcher(appContext, model.hashString, displayName))
}
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.text.TextUtils
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
@ -128,11 +127,10 @@ class ConversationView : LinearLayout {
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
}
binding.profilePictureView.update(thread.recipient)
binding.profilePictureView.load(thread.recipient)
}
fun recycle() {
binding.profilePictureView.recycle()
}
private fun getTitle(recipient: Recipient): String? = when {

View File

@ -347,8 +347,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
IdentityKeyUtil.checkUpdate(this)
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.update()
binding.profileButton.load(Address.fromSerialized(publicKey))
if (textSecurePreferences.getHasViewedSeed()) {
binding.seedReminderView.isVisible = false
}
@ -388,10 +387,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
private fun updateProfileButton() {
binding.profileButton.publicKey = publicKey
binding.profileButton.displayName = textSecurePreferences.getProfileName()
binding.profileButton.recycle()
binding.profileButton.update()
binding.profileButton.load(Address.fromSerialized(publicKey))
}
// endregion

View File

@ -55,8 +55,7 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) {
profilePictureView.publicKey = publicKey
profilePictureView.update(recipient)
profilePictureView.load(recipient)
nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener {
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener

View File

@ -11,7 +11,6 @@ import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
import network.loki.messenger.databinding.ViewGlobalSearchSubheaderBinding
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.ui.GetString
import java.security.InvalidParameterException
@ -99,12 +98,6 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is ContentView) {
holder.binding.searchResultProfilePicture.recycle()
}
}
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
val binding = ViewGlobalSearchResultBinding.bind(view)
@ -114,7 +107,6 @@ class GlobalSearchAdapter(private val modelCallback: (Model)->Unit): RecyclerVie
}
fun bind(query: String, model: Model) {
binding.searchResultProfilePicture.recycle()
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)

View File

@ -93,7 +93,7 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
binding.searchResultTimestamp.isVisible = false
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
binding.searchResultProfilePicture.update(threadRecipient)
binding.searchResultProfilePicture.load(threadRecipient)
val nameString = model.groupRecord.title
binding.searchResultTitle.text = getHighlight(query, nameString)
@ -111,7 +111,7 @@ fun ContentView.bindModel(query: String?, model: ContactModel) = binding.run {
searchResultTimestamp.isVisible = false
searchResultSubtitle.text = null
val recipient = Recipient.from(root.context, Address.fromSerialized(model.contact.accountID), false)
searchResultProfilePicture.update(recipient)
searchResultProfilePicture.load(recipient)
val nameString = if (model.isSelf) root.context.getString(R.string.note_to_self)
else model.contact.getSearchName()
searchResultTitle.text = getHighlight(query, nameString)
@ -121,7 +121,7 @@ fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self)
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
binding.searchResultProfilePicture.load(Address.fromSerialized(model.currentUserPublicKey))
binding.searchResultProfilePicture.isVisible = true
}
@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
// unreadCountTextView.text = model.unread.toString()
// }
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
searchResultProfilePicture.load(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind

View File

@ -49,12 +49,11 @@ class MessageRequestView : LinearLayout {
binding.snippetTextView.text = snippet
post {
binding.profilePictureView.update(thread.recipient)
binding.profilePictureView.load(thread.recipient)
}
}
fun recycle() {
binding.profilePictureView.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {

View File

@ -56,6 +56,7 @@ public class SignalGlideModule extends AppGlideModule {
// builder.setDiskCache(new NoopDiskCacheFactory());
}
/** @noinspection unchecked*/
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
@ -74,6 +75,7 @@ public class SignalGlideModule extends AppGlideModule {
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}

View File

@ -36,11 +36,6 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
else holder.select(getItem(position).isSelected)
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.binding.profilePictureView.recycle()
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val glide = Glide.with(itemView)
@ -48,9 +43,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
binding.recipientName.text = selectable.item.name
with (binding.profilePictureView) {
update(selectable.item)
}
binding.profilePictureView.load(selectable.item)
binding.root.setOnClickListener { toggle(selectable) }
binding.selectButton.isSelected = selectable.isSelected
}

View File

@ -168,7 +168,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
super.onStart()
binding.run {
setupProfilePictureView(profilePictureView)
loadProfilePicture(profilePictureView)
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = getDisplayName()
@ -185,12 +185,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
private fun getDisplayName(): String =
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
view.apply {
publicKey = hexEncodedPublicKey
displayName = getDisplayName()
update()
}
private fun loadProfilePicture(view: ProfilePictureView) {
// Always reload the profile picture as it can change on this page.
view.load(Address.fromSerialized(hexEncodedPublicKey))
}
override fun onSaveInstanceState(outState: Bundle) {
@ -331,9 +328,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
// Update our visuals
binding.profilePictureView.recycle()
binding.profilePictureView.update()
loadProfilePicture(binding.profilePictureView)
}
// If the sync failed then inform the user
@ -408,7 +403,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
cancelButton()
}.apply {
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
?.also(::setupProfilePictureView)
?.also(::loadProfilePicture)
val pictureIcon = findViewById<View>(R.id.ic_pictures)

View File

@ -13,7 +13,6 @@ import org.session.libsession.messaging.utilities.AccountId;
import org.thoughtcrime.securesms.components.ProfilePictureView;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.database.model.MessageId;
import com.bumptech.glide.Glide;
import java.util.Collections;
import java.util.List;
@ -154,7 +153,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
callback.onRemoveReaction(reaction.getBaseEmoji(), messageId, reaction.getTimestamp());
});
this.avatar.update(reaction.getSender());
this.avatar.load(reaction.getSender());
if (reaction.getSender().isLocalNumber()) {
this.recipient.setText(R.string.ReactionsRecipientAdapter_you);
@ -170,7 +169,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
}
void unbind() {
avatar.recycle();
}
}

View File

@ -11,7 +11,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -361,7 +360,7 @@ fun RowScope.Avatar(recipient: Recipient) {
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(recipient) }
ProfilePictureView(it).apply { load(recipient) }
},
modifier = Modifier
.width(46.dp)

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.conversation.v2.components.MentionCandidateView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:orientation="horizontal"
android:paddingStart="@dimen/medium_spacing"
android:paddingEnd="@dimen/medium_spacing"
android:gravity="center_vertical"
android:background="@drawable/mention_candidate_view_background">
<RelativeLayout
android:layout_width="26dp"
android:layout_height="32dp">
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"
android:layout_marginTop="3dp" />
<ImageView
android:id="@+id/moderatorIconImageView"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_crown"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<TextView
android:id="@+id/mentionCandidateNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing"
android:textSize="@dimen/small_font_size"
android:textColor="?android:textColorPrimary"
android:maxLines="1"
android:ellipsize="end" />
</org.thoughtcrime.securesms.conversation.v2.components.MentionCandidateView>

View File

@ -3,10 +3,12 @@ package org.session.libsession.avatars
import com.bumptech.glide.load.Key
import java.security.MessageDigest
class PlaceholderAvatarPhoto(val hashString: String,
val displayName: String): Key {
data class PlaceholderAvatarPhoto(
val hashString: String,
val displayName: String?
) : Key {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(hashString.encodeToByteArray())
messageDigest.update(displayName.encodeToByteArray())
messageDigest.update(displayName?.encodeToByteArray() ?: byteArrayOf())
}
}