mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-21 15:05:19 +00:00
WIP for new avatar selection
This commit is contained in:
parent
2174976716
commit
c38efc2ef8
@ -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(
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}*/
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user