Merge branch 'dev' into strings-squashed

This commit is contained in:
alansley 2024-08-21 13:28:16 +10:00
commit 2192c2c007
38 changed files with 482 additions and 305 deletions

View File

@ -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() {

View File

@ -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)

View File

@ -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()
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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> {

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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;
}

View File

@ -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")

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -131,6 +131,7 @@ fun MediaOverviewScreen(
onSaveClicked = { showingSaveAttachmentWarning = true },
onDeleteClicked = { showingDeleteConfirmation = true },
onSelectAllClicked = viewModel::onSelectAllClicked,
numSelected = selectedItems.size,
appBarScrollBehavior = appBarScrollBehavior
)
}

View File

@ -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 = {

View File

@ -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? {

View File

@ -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());
}

View File

@ -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)
}
}
}

View File

@ -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() {

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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 = {

View File

@ -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()
}
}

View File

@ -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
}
}

View 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>

View File

@ -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

View File

@ -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())
}
}