WIP for new avatar selection

This commit is contained in:
ThomasSession 2024-09-06 18:09:18 +10:00
parent 2174976716
commit c38efc2ef8
10 changed files with 472 additions and 175 deletions

View File

@ -74,8 +74,9 @@ class AvatarSelection(
*/
fun startAvatarSelection(
includeClear: Boolean,
attemptToIncludeCamera: Boolean
): File? {
attemptToIncludeCamera: Boolean,
createTempFile: ()->File?
) {
var captureFile: File? = null
val hasCameraPermission = ContextCompat
.checkSelfPermission(
@ -83,18 +84,11 @@ class AvatarSelection(
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
if (attemptToIncludeCamera && hasCameraPermission) {
try {
captureFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(activity))
} catch (e: IOException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
} catch (e: NoExternalStorageException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
}
captureFile = createTempFile()
}
val chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear)
onPickImage.launch(chooserIntent)
return captureFile
}
private fun createAvatarSelectionIntent(

View File

@ -572,7 +572,7 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_save_icon,
R.string.save,
{ handleActionItemClicked(Action.DOWNLOAD) },
R.string.AccessibilityId_save
R.string.AccessibilityId_saveAttachment
)
}
}

View File

@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager
@ -46,6 +48,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
@ -212,7 +215,15 @@ fun CellMetadata(
senderInfo?.let {
TitledView(state.fromTitle) {
Row {
sender?.let { Avatar(it) }
sender?.let {
Avatar(
recipient = it,
modifier = Modifier
.align(Alignment.CenterVertically)
.size(46.dp)
)
Spacer(modifier = Modifier.width(LocalDimensions.current.smallSpacing))
}
TitledMonospaceText(it)
}
}

View File

@ -34,7 +34,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@ -232,7 +231,7 @@ private fun SaveAttachmentWarningDialog(
title = context.getString(R.string.warning),
text = context.resources.getString(R.string.attachmentsWarning),
buttons = listOf(
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_save), color = LocalColors.current.danger, onClick = onAccepted),
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_saveAttachment), color = LocalColors.current.danger, onClick = onAccepted),
DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
)
)

View File

@ -6,6 +6,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
@ -13,38 +14,52 @@ import android.util.SparseArray
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.dimensionResource
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.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
@ -53,7 +68,6 @@ import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
@ -63,34 +77,36 @@ import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.debugmenu.DebugActivity
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.home.PathActivity
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.*
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.LargeItemButton
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.setThemedContent
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.dangerButtonColors
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.NetworkUtils
import org.thoughtcrime.securesms.util.push
@ -106,41 +122,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var prefs: TextSecurePreferences
private val viewModel: SettingsViewModel by viewModels()
private lateinit var binding: ActivitySettingsBinding
private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
private var tempFile: File? = null
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
private val onAvatarCropped = registerForActivityResult(CropImageContract()) { result ->
when {
result.isSuccessful -> {
Log.i(TAG, result.getUriFilePath(this).toString())
lifecycleScope.launch(Dispatchers.IO) {
try {
val profilePictureToBeUploaded =
BitmapUtil.createScaledBytes(
this@SettingsActivity,
result.getUriFilePath(this@SettingsActivity).toString(),
ProfileMediaConstraints()
).bitmap
launch(Dispatchers.Main) {
updateProfilePicture(profilePictureToBeUploaded)
}
} catch (e: BitmapDecodingException) {
Log.e(TAG, e)
}
}
}
result is CropImage.CancelledResult -> {
Log.i(TAG, "Cropping image was cancelled by the user")
}
else -> {
Log.e(TAG, "Cropping image failed")
}
}
viewModel.onAvatarPicked(result)
}
private val onPickImage = registerForActivityResult(
@ -149,12 +138,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
val inputFile: Uri? = result.data?.data ?: tempFile?.let(Uri::fromFile)
val inputFile: Uri? = result.data?.data ?: viewModel.getTempFile()?.let(Uri::fromFile)
cropImage(inputFile, outputFile)
}
private val avatarSelection = AvatarSelection(this, onAvatarCropped, onPickImage)
private var showAvatarDialog: Boolean by mutableStateOf(false)
companion object {
private const val SCROLL_STATE = "SCROLL_STATE"
}
@ -167,17 +158,37 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// set the toolbar icon to a close icon
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
// set the compose dialog content
binding.avatarDialog.setThemedContent {
if(showAvatarDialog){
AvatarDialogContainer(
saveAvatar = {
//todo TEMPORARY !!!!!!!!!!!!!!!!!!!!
(viewModel.avatarDialogState.value as? TempAvatar)?.let{ updateProfilePicture(it.data) }
},
removeAvatar = ::removeProfilePicture,
startAvatarSelection = ::startAvatarSelection
)
}
}
}
override fun onStart() {
super.onStart()
binding.run {
setupProfilePictureView(profilePictureView)
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
profilePictureView.apply {
publicKey = viewModel.hexEncodedPublicKey
displayName = viewModel.getDisplayName()
update()
}
profilePictureView.setOnClickListener {
showAvatarDialog = true
}
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = getDisplayName()
publicKeyTextView.text = hexEncodedPublicKey
btnGroupNameDisplay.text = viewModel.getDisplayName()
publicKeyTextView.text = viewModel.hexEncodedPublicKey
val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6)
val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}"
val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment"
@ -195,17 +206,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_bottom)
}
private fun getDisplayName(): String =
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
view.apply {
publicKey = hexEncodedPublicKey
displayName = getDisplayName()
update()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val scrollBundle = SparseArray<Parcelable>()
@ -310,6 +310,29 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
return updateWasSuccessful
}
// private fun createAvatarDialog(){
// if (avatarDialog != null) return
//
// avatarDialog = SettingsAvatarDialog(
// userKey = viewModel.hexEncodedPublicKey,
// userName = viewModel.getDisplayName(),
// startAvatarSelection = ::startAvatarSelection,
// saveAvatar = {
// viewModel.temporaryAvatar.value?.let{ updateProfilePicture(it) }
// },
// removeAvatar = ::removeProfilePicture
// )
//
// updateAvatarDialogImage(viewModel.temporaryAvatar.value)
// }
// private fun updateAvatarDialogImage(temporaryAvatar: ByteArray?){
// avatarDialog?.update(
// temporaryAvatar = temporaryAvatar,
// hasUserAvatar = viewModel.hasAvatar()
// )
// }
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
binding.loader.isVisible = true
@ -415,39 +438,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
return updateDisplayName(displayName)
}
private fun showEditProfilePictureUI() {
showSessionDialog {
title(R.string.profileDisplayPictureSet)
view(R.layout.dialog_change_avatar)
// Note: This is the only instance in a dialog where the "Save" button is not a `dangerButton`
button(R.string.save) { startAvatarSelection() }
if (prefs.getProfileAvatarId() != 0) {
button(R.string.remove) { removeProfilePicture() }
}
cancelButton()
}.apply {
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
?.also(::setupProfilePictureView)
val pictureIcon = findViewById<View>(R.id.ic_pictures)
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
profilePic?.isVisible = photoSet
pictureIcon?.isVisible = !photoSet
}
}
private fun startAvatarSelection() {
// Ask for an optional camera permission.
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.onAnyResult {
tempFile = avatarSelection.startAvatarSelection( false, true)
avatarSelection.startAvatarSelection(
includeClear = false,
attemptToIncludeCamera = true,
createTempFile = viewModel::createTempFile
)
}
.execute()
}
@ -574,6 +574,124 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
}
}
@Composable
fun AvatarDialogContainer(
startAvatarSelection: ()->Unit,
saveAvatar: ()->Unit,
removeAvatar: ()->Unit
){
val state by viewModel.avatarDialogState.collectAsState()
AvatarDialog(
state = state,
startAvatarSelection = startAvatarSelection,
saveAvatar = saveAvatar,
removeAvatar = removeAvatar
)
}
@Composable
fun AvatarDialog(
state: SettingsViewModel.AvatarDialogState,
startAvatarSelection: ()->Unit,
saveAvatar: ()->Unit,
removeAvatar: ()->Unit
){
AlertDialog(
onDismissRequest = {
viewModel.onAvatarDialogDismissed()
showAvatarDialog = false
},
title = stringResource(R.string.profileDisplayPictureSet),
content = {
Box(
modifier = Modifier
.padding(top = LocalDimensions.current.smallSpacing)
.size(dimensionResource(id = R.dimen.large_profile_picture_size))
.clickable {
startAvatarSelection()
}
.background(
shape = CircleShape,
color = LocalColors.current.backgroundBubbleReceived,
),
contentAlignment = Alignment.Center
) {
when(val s = state){
// user avatar
is UserAvatar -> {
Avatar(userAddress = s.address)
}
// temporary image
is TempAvatar -> {
Image(
modifier = Modifier.size(dimensionResource(id = R.dimen.large_profile_picture_size))
.clip(shape = CircleShape,),
bitmap = BitmapFactory.decodeByteArray(s.data, 0, s.data.size).asImageBitmap(),
contentDescription = null
)
}
// empty state
else -> {
Image(
modifier = Modifier.align(Alignment.Center),
painter = painterResource(id = R.drawable.ic_pictures),
contentDescription = null,
colorFilter = ColorFilter.tint(LocalColors.current.textSecondary)
)
}
}
Image(
modifier = Modifier
.size(LocalDimensions.current.spacing)
.background(
shape = CircleShape,
color = LocalColors.current.primary
)
.padding(LocalDimensions.current.xxxsSpacing)
.align(Alignment.BottomEnd)
,
painter = painterResource(id = R.drawable.ic_plus),
contentDescription = null,
colorFilter = ColorFilter.tint(Color.Black)
)
}
},
showCloseButton = true, // display the 'x' button
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.save),
contentDescription = GetString(R.string.AccessibilityId_save),
onClick = saveAvatar
),
DialogButtonModel(
text = GetString(R.string.remove),
contentDescription = GetString(R.string.AccessibilityId_remove),
onClick = removeAvatar
)
)
)
}
@Preview
@Composable
fun PreviewAvatarDialog(
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
){
PreviewTheme(colors) {
AvatarDialog(
state = NoAvatar,
startAvatarSelection = {},
saveAvatar = {},
removeAvatar = {}
)
}
}
}
private fun Context.hasPaths(): Flow<Boolean> = LocalBroadcastManager.getInstance(this).hasPaths()

View File

@ -0,0 +1,215 @@
package org.thoughtcrime.securesms.preferences
import android.content.Context
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageView
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.UserPic
import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.NoExternalStorageException
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.NetworkUtils
import java.io.File
import java.io.IOException
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
val prefs: TextSecurePreferences
) : ViewModel() {
private val TAG = "SettingsViewModel"
private var tempFile: File? = null
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
private val _avatarDialogState: MutableStateFlow<AvatarDialogState> = MutableStateFlow(
getDefaultAvatarDialogState()
)
val avatarDialogState: StateFlow<AvatarDialogState>
get() = _avatarDialogState
fun getDisplayName(): String =
prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
fun hasAvatar() = prefs.getProfileAvatarId() != 0
fun createTempFile(): File? {
try {
tempFile = File.createTempFile("avatar-capture", ".jpg", getImageDir(context))
} catch (e: IOException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
} catch (e: NoExternalStorageException) {
Log.e("Cannot reserve a temporary avatar capture file.", e)
}
return tempFile
}
fun getTempFile() = tempFile
fun onAvatarPicked(result: CropImageView.CropResult) {
when {
result.isSuccessful -> {
Log.i(TAG, result.getUriFilePath(context).toString())
viewModelScope.launch(Dispatchers.IO) {
try {
val profilePictureToBeUploaded =
BitmapUtil.createScaledBytes(
context,
result.getUriFilePath(context).toString(),
ProfileMediaConstraints()
).bitmap
// update dialog with temporary avatar (has not been saved/uploaded yet)
_avatarDialogState.value =
AvatarDialogState.TempAvatar(profilePictureToBeUploaded)
} catch (e: BitmapDecodingException) {
Log.e(TAG, e)
}
}
}
result is CropImage.CancelledResult -> {
Log.i(TAG, "Cropping image was cancelled by the user")
}
else -> {
Log.e(TAG, "Cropping image failed")
}
}
}
fun onAvatarDialogDismissed() {
_avatarDialogState.value =getDefaultAvatarDialogState()
}
fun getDefaultAvatarDialogState() = if (hasAvatar()) AvatarDialogState.UserAvatar(Address.fromSerialized(hexEncodedPublicKey))
else AvatarDialogState.NoAvatar
//todo properly close dialog when done and make sure the state is the right one post change
//todo make ripple effect round in dialog avatar picker
//todo link other states, like making sure we show the actual avatar if there's already one
//todo move upload and remove to VM
//todo make buttons in dialog disabled
//todo clean up the classes I made which aren't used now...
sealed class AvatarDialogState() {
object NoAvatar : AvatarDialogState()
data class UserAvatar(val address: Address) : AvatarDialogState()
data class TempAvatar(val data: ByteArray) : AvatarDialogState()
}
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
/*private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
binding.loader.isVisible = true
// Grab the profile key and kick of the promise to update the profile picture
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
val updateProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)
// If the online portion of the update succeeded then update the local state
updateProfilePicturePromise.successUi {
// When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
if (profilePicture.isEmpty()) {
MessagingModuleConfiguration.shared.storage.clearUserPic()
}
val userConfig = configFactory.user
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// Attempt to grab the details we require to update the profile picture
val url = prefs.getProfilePictureURL()
val profileKey = ProfileKeyUtil.getProfileKey(this)
// If we have a URL and a profile key then set the user's profile picture
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
userConfig?.setPic(UserPic(url, profileKey))
}
if (userConfig != null && userConfig.needsDump()) {
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
// Update our visuals
binding.profilePictureView.recycle()
binding.profilePictureView.update()
}
// If the sync failed then inform the user
updateProfilePicturePromise.failUi { onFail() }
// Finally, remove the loader animation after we've waited for the attempt to succeed or fail
updateProfilePicturePromise.alwaysUi { binding.loader.isVisible = false }
}
private fun updateProfilePicture(profilePicture: ByteArray) {
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
if (!haveNetworkConnection) {
Log.w(TAG, "Cannot update profile picture - no network connection.")
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
return
}
val onFail: () -> Unit = {
Log.e(TAG, "Sync failed when uploading profile picture.")
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
}
syncProfilePicture(profilePicture, onFail)
}
private fun removeProfilePicture() {
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
if (!haveNetworkConnection) {
Log.w(TAG, "Cannot remove profile picture - no network connection.")
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
return
}
val onFail: () -> Unit = {
Log.e(TAG, "Sync failed when removing profile picture.")
Toast.makeText(this@SettingsActivity, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
}
val emptyProfilePicture = ByteArray(0)
syncProfilePicture(emptyProfilePicture, onFail)
}*/
}

View File

@ -65,6 +65,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.OptionsCardData
@ -399,22 +400,31 @@ fun Divider(modifier: Modifier = Modifier, startIndent: Dp = 0.dp) {
)
}
//TODO This component should be fully rebuilt in Compose at some point ~~
@Composable
fun RowScope.Avatar(recipient: Recipient) {
Box(
modifier = Modifier
.width(60.dp)
.align(Alignment.CenterVertically)
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(recipient) }
},
modifier = Modifier
.width(46.dp)
.height(46.dp)
)
}
fun Avatar(
recipient: Recipient,
modifier: Modifier = Modifier
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(recipient) }
},
modifier = modifier
)
}
@Composable
fun Avatar(
userAddress: Address,
modifier: Modifier = Modifier
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(userAddress) }
},
modifier = modifier
)
}
@Composable

View File

@ -139,4 +139,9 @@
</FrameLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/avatarDialog"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:layout_width="match_parent">
<FrameLayout
android:layout_gravity="center"
android:id="@+id/ic_pictures"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/classic_dark_3"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size">
<ImageView
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:src="@drawable/ic_pictures"/>
<!-- TODO: Add this back when we build the custom modal which allows tapping on the image to select a replacement-->
<!-- <LinearLayout-->
<!-- android:layout_gravity="bottom|end"-->
<!-- android:gravity="center"-->
<!-- android:background="@drawable/circle_tintable"-->
<!-- android:backgroundTint="?attr/accentColor"-->
<!-- android:paddingTop="1dp"-->
<!-- android:paddingLeft="1dp"-->
<!-- android:layout_width="24dp"-->
<!-- android:layout_height="24dp"-->
<!-- tools:backgroundTint="@color/accent_green">-->
<!-- <View-->
<!-- android:background="@drawable/ic_plus"-->
<!-- android:backgroundTint="@color/black"-->
<!-- android:layout_width="12dp"-->
<!-- android:layout_height="12dp"-->
<!-- />-->
<!-- </LinearLayout>-->
</FrameLayout>
<org.thoughtcrime.securesms.components.ProfilePictureView
android:layout_margin="30dp"
android:id="@+id/profile_picture_view"
android:layout_gravity="center"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"
android:layout_marginTop="@dimen/medium_spacing"
android:contentDescription="@string/AccessibilityId_profilePicture"
tools:visibility="gone"/>
</FrameLayout>

View File

@ -43,6 +43,7 @@
<string name="AccessibilityId_contactUserDetails">Details</string>
<string name="AccessibilityId_pin">Pin</string>
<string name="AccessibilityId_profilePicture">User settings</string>
<string name="AccessibilityId_avatarPicker">Image picker</string>
<string name="AccessibilityId_searchIcon">Search icon</string>
<!--Settings Page -->
<string name="AccessibilityId_conversationsBlockedContacts">Blocked contacts</string>
@ -122,7 +123,7 @@
<string name="AccessibilityId_message">Message body</string>
<string name="AccessibilityId_sent">Message sent status: Sent</string>
<string name="AccessibilityId_reply">Reply to message</string>
<string name="AccessibilityId_save">Save attachment</string>
<string name="AccessibilityId_saveAttachment">Save attachment</string>
<string name="AccessibilityId_select">Select</string>
<string name="AccessibilityId_messageVoice">Voice message</string>
<string name="AccessibilityId_deliveryIndicator">Delivered</string>
@ -157,5 +158,7 @@
<string name="AccessibilityId_close">Close Dialog</string>
<string name="AccessibilityId_expand">Expand</string>
<string name="AccessibilityId_mediaMessage">Media message</string>
<string name="AccessibilityId_save">Save</string>
<string name="AccessibilityId_remove">Remove</string>
</resources>