mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 00:37:47 +00:00
Merge branch 'dev' into strings-squashed
This commit is contained in:
commit
2192c2c007
@ -280,8 +280,6 @@ dependencies {
|
||||
implementation 'pl.tajchert:waitingdots:0.1.0'
|
||||
implementation 'com.vanniktech:android-image-cropper:4.5.0'
|
||||
implementation 'com.melnykov:floatingactionbutton:1.3.0'
|
||||
implementation 'com.google.zxing:android-integration:3.1.0'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
@ -358,7 +356,6 @@ dependencies {
|
||||
testImplementation 'org.conscrypt:conscrypt-openjdk-uber:2.5.2' // For Robolectric
|
||||
testImplementation 'app.cash.turbine:turbine:1.1.0'
|
||||
|
||||
|
||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||
implementation "androidx.compose.animation:animation:$composeVersion"
|
||||
@ -377,7 +374,8 @@ dependencies {
|
||||
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
||||
implementation "androidx.camera:camera-view:1.3.2"
|
||||
|
||||
implementation "com.google.mlkit:barcode-scanning:17.2.0"
|
||||
// Note: ZXing 3.5.3 is the latest stable release as of 2024/08/21
|
||||
implementation "com.google.zxing:core:$zxingVersion"
|
||||
}
|
||||
|
||||
static def getLastCommitTimestamp() {
|
||||
|
@ -18,7 +18,6 @@ import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.session.libsignal.utilities.NoExternalStorageException
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||
import org.thoughtcrime.securesms.util.IntentUtils
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
@ -104,13 +103,8 @@ class AvatarSelection(
|
||||
includeClear: Boolean
|
||||
): Intent {
|
||||
val extraIntents: MutableList<Intent> = LinkedList()
|
||||
var galleryIntent = Intent(Intent.ACTION_PICK)
|
||||
galleryIntent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*")
|
||||
|
||||
if (!IntentUtils.isResolvable(context, galleryIntent)) {
|
||||
galleryIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
galleryIntent.setType("image/*")
|
||||
}
|
||||
val galleryIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
galleryIntent.setType("image/*")
|
||||
|
||||
if (tempCaptureFile != null) {
|
||||
val uri = FileProviderUtil.getUriFor(context, tempCaptureFile)
|
||||
|
@ -32,6 +32,7 @@ import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||
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
|
||||
@ -197,8 +198,13 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
clipFloatingInsets()
|
||||
|
||||
// set up the user avatar
|
||||
TextSecurePreferences.getLocalNumber(this)?.let {
|
||||
binding.userAvatar.load(Address.fromSerialized(it))
|
||||
TextSecurePreferences.getLocalNumber(this)?.let{
|
||||
val username = TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(it)
|
||||
binding.userAvatar.apply {
|
||||
publicKey = it
|
||||
displayName = username
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -335,6 +341,8 @@ 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)
|
||||
@ -342,7 +350,11 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
||||
binding.remoteRecipientName.text = contactDisplayName
|
||||
|
||||
// sort out the contact's avatar
|
||||
binding.contactAvatar.load(latestRecipient.recipient)
|
||||
binding.contactAvatar.apply {
|
||||
publicKey = contactPublicKey
|
||||
displayName = contactDisplayName
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,224 +3,164 @@ package org.thoughtcrime.securesms.components
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.bumptech.glide.Glide
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
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.TextSecurePreferences
|
||||
import org.session.libsession.utilities.AppTextSecurePreferences
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import javax.inject.Inject
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
|
||||
@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
|
||||
class ProfilePictureView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : RelativeLayout(context, attrs) {
|
||||
private val TAG = "ProfilePictureView"
|
||||
|
||||
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||
private var lastLoadJob: Job? = null
|
||||
private var lastLoadAddress: Address? = null
|
||||
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 val unknownRecipientDrawable by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ResourceContactPhoto(R.drawable.ic_profile_default)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
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 unknownOpenGroupDrawable by lazy(LazyThreadSafetyMode.NONE) {
|
||||
ResourceContactPhoto(R.drawable.ic_notification)
|
||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false)
|
||||
fun update(recipient: Recipient) {
|
||||
recipient.run { update(address, isClosedGroupRecipient, isOpenGroupInboxRecipient) }
|
||||
}
|
||||
|
||||
private fun setShowAsDoubleMode(showAsDouble: Boolean) {
|
||||
binding.doubleModeImageViewContainer.isVisible = showAsDouble
|
||||
binding.singleModeImageView.isVisible = !showAsDouble
|
||||
}
|
||||
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 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
when (groupAvatarOrMemberAvatars) {
|
||||
is ContactPhoto -> {
|
||||
setShowAsDoubleMode(false)
|
||||
Glide.with(this@ProfilePictureView)
|
||||
.load(groupAvatarOrMemberAvatars)
|
||||
.error(unknownRecipientDrawable)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(binding.singleModeImageView)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
Glide.with(binding.doubleModeImageView2)
|
||||
.load(second?.let { it.contactPhoto ?: it.placeholderAvatarPhoto })
|
||||
.error(second?.placeholderAvatarPhoto ?: unknownRecipientDrawable)
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(binding.doubleModeImageView2)
|
||||
}
|
||||
|
||||
else -> {
|
||||
setShowAsDoubleMode(false)
|
||||
binding.singleModeImageView.setImageDrawable(unknownRecipientDrawable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 if(isOpenGroupInboxRecipient) {
|
||||
val publicKey = GroupUtil.getDecodedOpenGroupInboxAccountId(address.serialize())
|
||||
this.publicKey = publicKey
|
||||
displayName = getUserDisplayName(publicKey)
|
||||
additionalPublicKey = null
|
||||
} else {
|
||||
loadAsSingleImage(LoadModel.RecipientModel(recipient))
|
||||
val publicKey = address.serialize()
|
||||
this.publicKey = publicKey
|
||||
displayName = getUserDisplayName(publicKey)
|
||||
additionalPublicKey = null
|
||||
}
|
||||
|
||||
lastLoadAddress = recipient.address
|
||||
update()
|
||||
}
|
||||
|
||||
fun load(address: Address) {
|
||||
if (address.isClosedGroup) {
|
||||
loadAsDoubleImages(LoadModel.AddressModel(address))
|
||||
} else {
|
||||
loadAsSingleImage(LoadModel.AddressModel(address))
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
// clear multi image
|
||||
glide.clear(binding.doubleModeImageView1)
|
||||
glide.clear(binding.doubleModeImageView2)
|
||||
binding.doubleModeImageViewContainer.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
lastLoadAddress = address
|
||||
}
|
||||
|
||||
private fun Recipient.displayName(): String {
|
||||
return if (isLocalNumber) {
|
||||
TextSecurePreferences.getProfileName(context).orEmpty()
|
||||
} else {
|
||||
profileName ?: name ?: ""
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
private sealed interface LoadModel {
|
||||
val address: Address
|
||||
glide.clear(imageView)
|
||||
|
||||
/**
|
||||
* Load the recipient if it's not already loaded.
|
||||
*/
|
||||
fun loadRecipient(context: Context): Recipient
|
||||
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||
|
||||
data class AddressModel(override val address: Address) : LoadModel {
|
||||
override fun loadRecipient(context: Context): Recipient {
|
||||
return Recipient.from(context, address, false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
data class RecipientModel(val recipient: Recipient) : LoadModel {
|
||||
override val address: Address
|
||||
get() = recipient.address
|
||||
|
||||
override fun loadRecipient(context: Context): Recipient = recipient
|
||||
} else {
|
||||
glide.load(unknownRecipientDrawable)
|
||||
.centerCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
profilePicturesCache.clear()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class UserView : LinearLayout {
|
||||
}
|
||||
|
||||
val address = user.address.serialize()
|
||||
binding.profilePictureView.load(user)
|
||||
binding.profilePictureView.update(user)
|
||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||
when (actionIndicator) {
|
||||
@ -86,6 +86,8 @@ class UserView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fun unbind() { /* Nothing to do */ }
|
||||
fun unbind() {
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -85,9 +85,8 @@ class ConversationActionBarView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun update(recipient: Recipient, openGroup: OpenGroup? = null, config: ExpirationConfiguration? = null) {
|
||||
binding.profilePictureView.load(recipient)
|
||||
binding.profilePictureView.update(recipient)
|
||||
binding.conversationTitleView.text = recipient.takeUnless { it.isLocalNumber }?.toShortString() ?: context.getString(R.string.noteToSelf)
|
||||
|
||||
updateSubtitle(recipient, openGroup, config)
|
||||
|
||||
binding.conversationTitleContainer.modifyLayoutParams<MarginLayoutParams> {
|
||||
|
@ -48,7 +48,7 @@ import org.thoughtcrime.securesms.ui.LoadingArcOr
|
||||
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
|
||||
import org.thoughtcrime.securesms.ui.components.BackAppBar
|
||||
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
|
||||
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||
@ -89,7 +89,7 @@ internal fun NewMessage(
|
||||
HorizontalPager(pagerState) {
|
||||
when (TITLES[it]) {
|
||||
R.string.accountIdEnter -> EnterAccountId(state, callbacks, onHelp)
|
||||
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = callbacks::onScanQrCode)
|
||||
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = callbacks::onScanQrCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ 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
|
||||
|
||||
|
@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -2,11 +2,13 @@ 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.load(Address.fromSerialized(candidate.member.publicKey))
|
||||
profilePictureView.publicKey = candidate.member.publicKey
|
||||
profilePictureView.displayName = candidate.member.name
|
||||
profilePictureView.additionalPublicKey = null
|
||||
profilePictureView.update()
|
||||
moderatorIconImageView.visibility = if (candidate.member.isModerator) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ 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
|
||||
@ -25,8 +26,6 @@ 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 java.util.Date
|
||||
import java.util.Locale
|
||||
@ -60,6 +59,8 @@ 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
|
||||
@ -178,7 +179,8 @@ class VisibleMessageView : FrameLayout {
|
||||
|
||||
if (isGroupThread && !message.isOutgoing) {
|
||||
if (isEndOfMessageCluster) {
|
||||
binding.profilePictureView.load(message.individualRecipient)
|
||||
binding.profilePictureView.publicKey = senderAccountID
|
||||
binding.profilePictureView.update(message.individualRecipient)
|
||||
binding.profilePictureView.setOnClickListener {
|
||||
if (thread.isCommunityRecipient) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||
@ -463,6 +465,7 @@ class VisibleMessageView : FrameLayout {
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
binding.messageContentView.root.recycle()
|
||||
}
|
||||
|
||||
|
@ -13,14 +13,14 @@ import org.session.libsession.avatars.ContactPhoto;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ContactPhotoFetcher implements DataFetcher<InputStream> {
|
||||
class ContactPhotoFetcher implements DataFetcher<InputStream> {
|
||||
|
||||
private final Context context;
|
||||
private final ContactPhoto contactPhoto;
|
||||
|
||||
private InputStream inputStream;
|
||||
|
||||
public ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
|
||||
ContactPhotoFetcher(@NonNull Context context, @NonNull ContactPhoto contactPhoto) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.contactPhoto = contactPhoto;
|
||||
}
|
||||
|
@ -9,15 +9,12 @@ 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 hashString: String,
|
||||
private val displayName: String
|
||||
): DataFetcher<BitmapDrawable> {
|
||||
class PlaceholderAvatarFetcher(private val context: Context,
|
||||
private val photo: PlaceholderAvatarPhoto): DataFetcher<BitmapDrawable> {
|
||||
|
||||
override fun loadData(priority: Priority,callback: DataFetcher.DataCallback<in BitmapDrawable>) {
|
||||
try {
|
||||
val avatar = AvatarPlaceholderGenerator.generate(context, 128, hashString, displayName)
|
||||
val avatar = AvatarPlaceholderGenerator.generate(context, 128, photo.hashString, photo.displayName)
|
||||
callback.onDataReady(avatar)
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Error in fetching avatar")
|
||||
|
@ -8,9 +8,6 @@ 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> {
|
||||
|
||||
@ -20,15 +17,7 @@ class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<Plac
|
||||
height: Int,
|
||||
options: Options
|
||||
): LoadData<BitmapDrawable> {
|
||||
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))
|
||||
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
|
||||
}
|
||||
|
||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||
|
@ -4,6 +4,7 @@ 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
|
||||
@ -127,10 +128,11 @@ 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.load(thread.recipient)
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
|
||||
private fun getTitle(recipient: Recipient): String? = when {
|
||||
|
@ -478,7 +478,8 @@ 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.load(Address.fromSerialized(publicKey))
|
||||
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||
binding.profileButton.update()
|
||||
if (textSecurePreferences.getHasViewedSeed()) {
|
||||
binding.seedReminderView.isVisible = false
|
||||
}
|
||||
@ -518,7 +519,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
}
|
||||
|
||||
private fun updateProfileButton() {
|
||||
binding.profileButton.load(Address.fromSerialized(publicKey))
|
||||
binding.profileButton.publicKey = publicKey
|
||||
binding.profileButton.displayName = textSecurePreferences.getProfileName()
|
||||
binding.profileButton.recycle()
|
||||
binding.profileButton.update()
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
@ -55,7 +55,8 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||
with(binding) {
|
||||
profilePictureView.load(recipient)
|
||||
profilePictureView.publicKey = publicKey
|
||||
profilePictureView.update(recipient)
|
||||
nameTextViewContainer.visibility = View.VISIBLE
|
||||
nameTextViewContainer.setOnClickListener {
|
||||
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener
|
||||
|
@ -11,6 +11,7 @@ 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
|
||||
@ -98,6 +99,12 @@ 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)
|
||||
@ -107,6 +114,7 @@ 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)
|
||||
|
@ -92,7 +92,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.load(threadRecipient)
|
||||
binding.searchResultProfilePicture.update(threadRecipient)
|
||||
val nameString = model.groupRecord.title
|
||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||
|
||||
@ -110,7 +110,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.load(recipient)
|
||||
searchResultProfilePicture.update(recipient)
|
||||
val nameString = if (model.isSelf) root.context.getString(R.string.noteToSelf)
|
||||
else model.contact.getSearchName()
|
||||
searchResultTitle.text = getHighlight(query, nameString)
|
||||
@ -120,7 +120,7 @@ fun ContentView.bindModel(model: SavedMessages) {
|
||||
binding.searchResultSubtitle.isVisible = false
|
||||
binding.searchResultTimestamp.isVisible = false
|
||||
binding.searchResultTitle.setText(R.string.noteToSelf)
|
||||
binding.searchResultProfilePicture.load(Address.fromSerialized(model.currentUserPublicKey))
|
||||
binding.searchResultProfilePicture.update(Address.fromSerialized(model.currentUserPublicKey))
|
||||
binding.searchResultProfilePicture.isVisible = true
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ fun ContentView.bindModel(query: String?, model: Message) = binding.apply {
|
||||
// }
|
||||
|
||||
searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||
searchResultProfilePicture.load(model.messageResult.conversationRecipient)
|
||||
searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||
val textSpannable = SpannableStringBuilder()
|
||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||
// group chat, bind
|
||||
|
@ -131,6 +131,7 @@ fun MediaOverviewScreen(
|
||||
onSaveClicked = { showingSaveAttachmentWarning = true },
|
||||
onDeleteClicked = { showingDeleteConfirmation = true },
|
||||
onSelectAllClicked = viewModel::onSelectAllClicked,
|
||||
numSelected = selectedItems.size,
|
||||
appBarScrollBehavior = appBarScrollBehavior
|
||||
)
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun MediaOverviewTopAppBar(
|
||||
selectionMode: Boolean,
|
||||
numSelected: Int,
|
||||
title: String,
|
||||
onBackClicked: () -> Unit,
|
||||
onSaveClicked: () -> Unit,
|
||||
@ -25,7 +26,8 @@ fun MediaOverviewTopAppBar(
|
||||
) {
|
||||
ActionAppBar(
|
||||
title = title,
|
||||
navigationIcon = {AppBarBackIcon(onBack = onBackClicked)},
|
||||
actionModeTitle = numSelected.toString(),
|
||||
navigationIcon = { AppBarBackIcon(onBack = onBackClicked) },
|
||||
scrollBehavior = appBarScrollBehavior,
|
||||
actionMode = selectionMode,
|
||||
actionModeActions = {
|
||||
|
@ -49,11 +49,12 @@ class MessageRequestView : LinearLayout {
|
||||
binding.snippetTextView.text = snippet
|
||||
|
||||
post {
|
||||
binding.profilePictureView.load(thread.recipient)
|
||||
binding.profilePictureView.update(thread.recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
binding.profilePictureView.recycle()
|
||||
}
|
||||
|
||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||
|
@ -56,7 +56,6 @@ 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();
|
||||
@ -75,7 +74,6 @@ 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());
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
|
||||
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
@ -52,7 +52,7 @@ internal fun LoadAccountScreen(
|
||||
) { page ->
|
||||
when (TITLES[page]) {
|
||||
R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue)
|
||||
R.string.qrScan -> MaybeScanQrCode(qrErrors, onScan = onScan)
|
||||
R.string.qrScan -> QRScannerScreen(qrErrors, onScan = onScan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
@ -14,7 +15,6 @@ import org.thoughtcrime.securesms.onboarding.manager.LoadAccountManager
|
||||
import org.thoughtcrime.securesms.onboarding.messagenotifications.MessageNotificationsActivity
|
||||
import org.thoughtcrime.securesms.ui.setComposeContent
|
||||
import org.thoughtcrime.securesms.util.start
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LoadAccountActivity : BaseActionBarActivity() {
|
||||
|
@ -4,6 +4,7 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -17,7 +18,6 @@ import org.session.libsignal.crypto.MnemonicCodec
|
||||
import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InputTooShort
|
||||
import org.session.libsignal.crypto.MnemonicCodec.DecodingError.InvalidWord
|
||||
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoadAccountEvent(val mnemonic: ByteArray)
|
||||
|
||||
@ -54,6 +54,7 @@ internal class LoadAccountViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun onScanQrCode(string: String) {
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
codec.decodeMnemonicOrHexAsByteArray(string).let(::onSuccess)
|
||||
|
@ -36,6 +36,11 @@ 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)
|
||||
@ -43,7 +48,9 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
|
||||
|
||||
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
|
||||
binding.recipientName.text = selectable.item.name
|
||||
binding.profilePictureView.load(selectable.item)
|
||||
with (binding.profilePictureView) {
|
||||
update(selectable.item)
|
||||
}
|
||||
binding.root.setOnClickListener { toggle(selectable) }
|
||||
binding.selectButton.isSelected = selectable.isSelected
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.database.threadDatabase
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
|
||||
import org.thoughtcrime.securesms.ui.components.QrImage
|
||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||
import org.thoughtcrime.securesms.ui.contentDescription
|
||||
@ -54,7 +54,7 @@ class QRCodeActivity : PassphraseRequiredActionBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onScan(string: String) {
|
||||
private fun onScan(string: String) {
|
||||
if (!PublicKeyValidation.isValid(string)) {
|
||||
errors.tryEmit(getString(R.string.qrNotAccountId))
|
||||
} else if (!isFinishing) {
|
||||
@ -83,7 +83,7 @@ private fun Tabs(accountId: String, errors: Flow<String>, onScan: (String) -> Un
|
||||
) { page ->
|
||||
when (TITLES[page]) {
|
||||
R.string.view -> QrPage(accountId)
|
||||
R.string.scan -> MaybeScanQrCode(errors, onScan = onScan)
|
||||
R.string.scan -> QRScannerScreen(errors, onScan = onScan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
super.onStart()
|
||||
|
||||
binding.run {
|
||||
loadProfilePicture(profilePictureView)
|
||||
setupProfilePictureView(profilePictureView)
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||
btnGroupNameDisplay.text = getDisplayName()
|
||||
@ -190,9 +190,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private fun getDisplayName(): String =
|
||||
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
|
||||
|
||||
private fun loadProfilePicture(view: ProfilePictureView) {
|
||||
// Always reload the profile picture as it can change on this page.
|
||||
view.load(Address.fromSerialized(hexEncodedPublicKey))
|
||||
private fun setupProfilePictureView(view: ProfilePictureView) {
|
||||
view.apply {
|
||||
publicKey = hexEncodedPublicKey
|
||||
displayName = getDisplayName()
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@ -333,7 +336,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
||||
|
||||
loadProfilePicture(binding.profilePictureView)
|
||||
// Update our visuals
|
||||
binding.profilePictureView.recycle()
|
||||
binding.profilePictureView.update()
|
||||
}
|
||||
|
||||
// If the sync failed then inform the user
|
||||
@ -412,7 +417,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
cancelButton()
|
||||
}.apply {
|
||||
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
|
||||
?.also(::loadProfilePicture)
|
||||
?.also(::setupProfilePictureView)
|
||||
|
||||
val pictureIcon = findViewById<View>(R.id.ic_pictures)
|
||||
|
||||
|
@ -14,6 +14,7 @@ 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;
|
||||
|
||||
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
|
||||
|
||||
@ -152,7 +153,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
||||
callback.onRemoveReaction(reaction.getBaseEmoji(), messageId, reaction.getTimestamp());
|
||||
});
|
||||
|
||||
this.avatar.load(reaction.getSender());
|
||||
this.avatar.update(reaction.getSender());
|
||||
|
||||
if (reaction.getSender().isLocalNumber()) {
|
||||
this.recipient.setText(R.string.you);
|
||||
@ -170,6 +171,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
avatar.recycle();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ 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
|
||||
@ -360,7 +361,7 @@ fun RowScope.Avatar(recipient: Recipient) {
|
||||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
ProfilePictureView(it).apply { load(recipient) }
|
||||
ProfilePictureView(it).apply { update(recipient) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.width(46.dp)
|
||||
|
@ -2,26 +2,27 @@ package org.thoughtcrime.securesms.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ui.Divider
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalColors
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
|
||||
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
|
||||
@ -37,7 +38,10 @@ fun AppBarPreview(
|
||||
Column() {
|
||||
BasicAppBar(title = "Basic App Bar")
|
||||
Divider()
|
||||
BasicAppBar(title = "Basic App Bar With Color", backgroundColor = LocalColors.current.backgroundSecondary)
|
||||
BasicAppBar(
|
||||
title = "Basic App Bar With Color",
|
||||
backgroundColor = LocalColors.current.backgroundSecondary
|
||||
)
|
||||
Divider()
|
||||
BackAppBar(title = "Back Bar", onBack = {})
|
||||
Divider()
|
||||
@ -69,7 +73,7 @@ fun BasicAppBar(
|
||||
backgroundColor: Color = LocalColors.current.background,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
){
|
||||
) {
|
||||
CenterAlignedTopAppBar(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
@ -94,7 +98,7 @@ fun BackAppBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
backgroundColor: Color = LocalColors.current.background,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
){
|
||||
) {
|
||||
BasicAppBar(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
@ -115,6 +119,7 @@ fun ActionAppBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
backgroundColor: Color = LocalColors.current.background,
|
||||
actionMode: Boolean = false,
|
||||
actionModeTitle: String = "",
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
actionModeActions: @Composable (RowScope.() -> Unit) = {},
|
||||
@ -126,7 +131,19 @@ fun ActionAppBar(
|
||||
AppBarText(title = title)
|
||||
}
|
||||
},
|
||||
navigationIcon =navigationIcon,
|
||||
navigationIcon = {
|
||||
if (actionMode) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.xxxsSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
navigationIcon()
|
||||
AppBarText(title = actionModeTitle)
|
||||
}
|
||||
} else {
|
||||
navigationIcon()
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = appBarColors(backgroundColor),
|
||||
actions = {
|
||||
|
@ -47,12 +47,16 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.ChecksumException
|
||||
import com.google.zxing.FormatException
|
||||
import com.google.zxing.NotFoundException
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.qrcode.QRCodeReader
|
||||
import com.squareup.phrase.Phrase
|
||||
import java.util.concurrent.Executors
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
@ -60,13 +64,12 @@ import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
|
||||
import org.thoughtcrime.securesms.ui.theme.LocalType
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
private const val TAG = "NewMessageFragment"
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun MaybeScanQrCode(
|
||||
fun QRScannerScreen(
|
||||
errors: Flow<String>,
|
||||
onClickSettings: () -> Unit = LocalContext.current.run { {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
@ -147,17 +150,13 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
|
||||
runCatching {
|
||||
cameraProvider.get().unbindAll()
|
||||
|
||||
val options = BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||
.build()
|
||||
val scanner = BarcodeScanning.getClient(options)
|
||||
|
||||
cameraProvider.get().bindToLifecycle(
|
||||
LocalLifecycleOwner.current,
|
||||
selector,
|
||||
preview,
|
||||
buildAnalysisUseCase(scanner, onScan)
|
||||
buildAnalysisUseCase(QRCodeReader(), onScan)
|
||||
)
|
||||
|
||||
}.onFailure { Log.e(TAG, "error binding camera", it) }
|
||||
|
||||
DisposableEffect(cameraProvider) {
|
||||
@ -221,32 +220,51 @@ fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun buildAnalysisUseCase(
|
||||
scanner: BarcodeScanner,
|
||||
scanner: QRCodeReader,
|
||||
onBarcodeScanned: (String) -> Unit
|
||||
): ImageAnalysis = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build().apply {
|
||||
setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned))
|
||||
setAnalyzer(Executors.newSingleThreadExecutor(), QRCodeAnalyzer(scanner, onBarcodeScanned))
|
||||
}
|
||||
|
||||
class Analyzer(
|
||||
private val scanner: BarcodeScanner,
|
||||
class QRCodeAnalyzer(
|
||||
private val qrCodeReader: QRCodeReader,
|
||||
private val onBarcodeScanned: (String) -> Unit
|
||||
): ImageAnalysis.Analyzer {
|
||||
|
||||
// Note: This analyze method is called once per frame of the camera feed.
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun analyze(image: ImageProxy) {
|
||||
InputImage.fromMediaImage(
|
||||
image.image!!,
|
||||
image.imageInfo.rotationDegrees
|
||||
).let(scanner::process).apply {
|
||||
addOnSuccessListener { barcodes ->
|
||||
barcodes.forEach {
|
||||
it.rawValue?.let(onBarcodeScanned)
|
||||
}
|
||||
}
|
||||
addOnCompleteListener {
|
||||
image.close()
|
||||
}
|
||||
// Grab the image data as a byte array so we can generate a PlanarYUVLuminanceSource from it
|
||||
val buffer = image.planes[0].buffer
|
||||
buffer.rewind()
|
||||
val imageBytes = ByteArray(buffer.capacity())
|
||||
buffer.get(imageBytes) // IMPORTANT: This transfers data from the buffer INTO the imageBytes array, although it looks like it would go the other way around!
|
||||
|
||||
// ZXing requires data as a BinaryBitmap to scan for QR codes, and to generate that we need to feed it a PlanarYUVLuminanceSource
|
||||
val luminanceSource = PlanarYUVLuminanceSource(imageBytes, image.width, image.height, 0, 0, image.width, image.height, false)
|
||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource))
|
||||
|
||||
// Attempt to extract a QR code from the binary bitmap, and pass it through to our `onBarcodeScanned` method if we find one
|
||||
try {
|
||||
val result: Result = qrCodeReader.decode(binaryBitmap)
|
||||
val resultTxt = result.text
|
||||
// No need to close the image here - it'll always make it to the end, and calling `onBarcodeScanned`
|
||||
// with a valid contact / recovery phrase / community code will stop calling this `analyze` method.
|
||||
onBarcodeScanned(resultTxt)
|
||||
}
|
||||
catch (nfe: NotFoundException) { /* Hits if there is no QR code in the image */ }
|
||||
catch (fe: FormatException) { /* Hits if we found a QR code but failed to decode it */ }
|
||||
catch (ce: ChecksumException) { /* Hits if we found a QR code which is corrupted */ }
|
||||
catch (e: Exception) {
|
||||
// Hits if there's a genuine problem
|
||||
Log.e("QR", "error", e)
|
||||
}
|
||||
|
||||
// Remember to close the image when we're done with it!
|
||||
// IMPORTANT: It is CLOSING the image that allows this method to run again! If we don't
|
||||
// close the image this method runs precisely ONCE and that's it, which is essentially useless.
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import org.session.libsignal.utilities.Log
|
||||
|
||||
object QRCodeUtilities {
|
||||
|
||||
@ -34,5 +35,8 @@ object QRCodeUtilities {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}.getOrElse {
|
||||
Log.e("QRCodeUtilities", "Failed to generate QR Code", it)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
42
app/src/main/res/layout/view_mention_candidate.xml
Normal file
42
app/src/main/res/layout/view_mention_candidate.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?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>
|
@ -40,6 +40,7 @@ phraseVersion=1.2.0
|
||||
preferenceVersion=1.2.0
|
||||
protobufVersion=2.5.0
|
||||
testCoreVersion=1.5.0
|
||||
zxingVersion=3.5.3
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
@ -3,12 +3,10 @@ package org.session.libsession.avatars
|
||||
import com.bumptech.glide.load.Key
|
||||
import java.security.MessageDigest
|
||||
|
||||
data class PlaceholderAvatarPhoto(
|
||||
val hashString: String,
|
||||
val displayName: String?
|
||||
) : Key {
|
||||
class PlaceholderAvatarPhoto(val hashString: String,
|
||||
val displayName: String): Key {
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(hashString.encodeToByteArray())
|
||||
messageDigest.update(displayName?.encodeToByteArray() ?: byteArrayOf())
|
||||
messageDigest.update(displayName.encodeToByteArray())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user