diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt index 2bded3cccb..04acb8908b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/WebRtcCallActivity.kt @@ -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) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 9511bddb6a..f2ea090179 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -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() - 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) + } } - // endregion -} + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index e0ca2a4242..3af63670dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt index 8f2da7a733..d8ba8122ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActionBarView.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 1c57dc8d5f..0a0829a02c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt deleted file mode 100644 index 5698ddd0bb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt +++ /dev/null @@ -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() - 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() - 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, 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt deleted file mode 100644 index 14dc6263ab..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt index f790e7f1c6..c1df0cb631 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 9f7f620ab5..5f26d50251 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java index 6ab528b785..7f74365604 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/ContactPhotoFetcher.java @@ -13,14 +13,14 @@ import org.session.libsession.avatars.ContactPhoto; import java.io.IOException; import java.io.InputStream; -class ContactPhotoFetcher implements DataFetcher { +public class ContactPhotoFetcher implements DataFetcher { 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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarFetcher.kt index 38d51877de..36ff89580f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarFetcher.kt @@ -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 { +class PlaceholderAvatarFetcher( + private val context: Context, + private val hashString: String, + private val displayName: String +): DataFetcher { override fun loadData(priority: Priority,callback: DataFetcher.DataCallback) { 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") diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarLoader.kt index b163b5ed90..235161ae32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarLoader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/PlaceholderAvatarLoader.kt @@ -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 { @@ -17,7 +20,15 @@ class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader { - 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 68aea84417..875c96ed23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 2d683abe8f..bd8919ddc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index cae399dcbf..a6d0627b27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index 71c2c62506..f70053e44a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index d390776d1c..7ee46b8c8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index 1fb1f38ce4..6e5ca6d8cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -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? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 02172b7248..ed8d3c7d85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -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()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt index e59d86c912..589aabef1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsAdapter.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fc52541987..72e9a5e1d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -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(R.id.profile_picture_view) - ?.also(::setupProfilePictureView) + ?.also(::loadProfilePicture) val pictureIcon = findViewById(R.id.ic_pictures) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java index 79717eabb1..c9a3f4ea13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -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 - - - - - - - - - - - - - \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/avatars/PlaceholderAvatarPhoto.kt b/libsession/src/main/java/org/session/libsession/avatars/PlaceholderAvatarPhoto.kt index 916e9112de..154825179c 100644 --- a/libsession/src/main/java/org/session/libsession/avatars/PlaceholderAvatarPhoto.kt +++ b/libsession/src/main/java/org/session/libsession/avatars/PlaceholderAvatarPhoto.kt @@ -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()) } } \ No newline at end of file