mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-24 18:45:19 +00:00
SES-2651 - New avatar picker
Updated the avatar picker to match the designs. Had to rewrite it in Compose and moved the logic to the VM. We could moev the whole settings page to compose in another step now that we have the VM set up.
This commit is contained in:
parent
c6333384da
commit
c0bf015049
@ -457,39 +457,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void resubmitProfilePictureIfNeeded() {
|
private void resubmitProfilePictureIfNeeded() {
|
||||||
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
ProfilePictureUtilities.INSTANCE.resubmitProfilePictureIfNeeded(this);
|
||||||
// at a certain interval to ensure it's always available.
|
|
||||||
String userPublicKey = TextSecurePreferences.getLocalNumber(this);
|
|
||||||
if (userPublicKey == null) return;
|
|
||||||
long now = new Date().getTime();
|
|
||||||
long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this);
|
|
||||||
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return;
|
|
||||||
ThreadUtils.queue(() -> {
|
|
||||||
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
|
||||||
Log.d("Loki-Avatar", "Uploading Avatar Started");
|
|
||||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this);
|
|
||||||
try {
|
|
||||||
// Read the file into a byte array
|
|
||||||
InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey));
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
int count;
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
|
||||||
baos.write(buffer, 0, count);
|
|
||||||
}
|
|
||||||
baos.flush();
|
|
||||||
byte[] profilePicture = baos.toByteArray();
|
|
||||||
// Re-upload it
|
|
||||||
ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> {
|
|
||||||
// Update the last profile picture upload date
|
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime());
|
|
||||||
Log.d("Loki-Avatar", "Uploading Avatar Finished");
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e("Loki-Avatar", "Uploading avatar failed.");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadEmojiSearchIndexIfNeeded() {
|
private void loadEmojiSearchIndexIfNeeded() {
|
||||||
|
@ -23,12 +23,12 @@ import androidx.compose.animation.Crossfade
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@ -37,6 +37,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -44,6 +45,7 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -51,38 +53,30 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.core.view.isInvisible
|
import androidx.core.view.isInvisible
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.canhub.cropper.CropImageContract
|
import com.canhub.cropper.CropImageContract
|
||||||
import com.squareup.phrase.Phrase
|
import com.squareup.phrase.Phrase
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.BuildConfig
|
import network.loki.messenger.BuildConfig
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivitySettingsBinding
|
import network.loki.messenger.databinding.ActivitySettingsBinding
|
||||||
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.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
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.SSKEnvironment.ProfileManagerProtocol
|
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
|
||||||
import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
|
import org.session.libsession.utilities.StringSubstitutionConstants.VERSION_KEY
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||||
import org.thoughtcrime.securesms.debugmenu.DebugActivity
|
import org.thoughtcrime.securesms.debugmenu.DebugActivity
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
|
||||||
import org.thoughtcrime.securesms.home.PathActivity
|
import org.thoughtcrime.securesms.home.PathActivity
|
||||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
@ -97,6 +91,7 @@ import org.thoughtcrime.securesms.ui.Divider
|
|||||||
import org.thoughtcrime.securesms.ui.GetString
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import org.thoughtcrime.securesms.ui.LargeItemButton
|
import org.thoughtcrime.securesms.ui.LargeItemButton
|
||||||
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
|
import org.thoughtcrime.securesms.ui.LargeItemButtonWithDrawable
|
||||||
|
import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator
|
||||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
|
import org.thoughtcrime.securesms.ui.components.PrimaryOutlineCopyButton
|
||||||
import org.thoughtcrime.securesms.ui.contentDescription
|
import org.thoughtcrime.securesms.ui.contentDescription
|
||||||
@ -117,8 +112,6 @@ import javax.inject.Inject
|
|||||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private val TAG = "SettingsActivity"
|
private val TAG = "SettingsActivity"
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var configFactory: ConfigFactory
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var prefs: TextSecurePreferences
|
lateinit var prefs: TextSecurePreferences
|
||||||
|
|
||||||
@ -163,19 +156,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.avatarDialog.setThemedContent {
|
binding.avatarDialog.setThemedContent {
|
||||||
if(showAvatarDialog){
|
if(showAvatarDialog){
|
||||||
AvatarDialogContainer(
|
AvatarDialogContainer(
|
||||||
saveAvatar = {
|
saveAvatar = viewModel::saveAvatar,
|
||||||
//todo TEMPORARY !!!!!!!!!!!!!!!!!!!!
|
removeAvatar = viewModel::removeAvatar,
|
||||||
(viewModel.avatarDialogState.value as? TempAvatar)?.let{ updateProfilePicture(it.data) }
|
|
||||||
},
|
|
||||||
removeAvatar = ::removeProfilePicture,
|
|
||||||
startAvatarSelection = ::startAvatarSelection
|
startAvatarSelection = ::startAvatarSelection
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
|
|
||||||
binding.run {
|
binding.run {
|
||||||
profilePictureView.apply {
|
profilePictureView.apply {
|
||||||
@ -184,6 +170,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
profilePictureView.setOnClickListener {
|
profilePictureView.setOnClickListener {
|
||||||
|
binding.avatarDialog.isVisible = true
|
||||||
showAvatarDialog = true
|
showAvatarDialog = true
|
||||||
}
|
}
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
@ -199,6 +186,25 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.composeView.setThemedContent {
|
binding.composeView.setThemedContent {
|
||||||
Buttons()
|
Buttons()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.showLoader.collect {
|
||||||
|
binding.loader.isVisible = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.refreshAvatar.collect {
|
||||||
|
binding.profilePictureView.recycle()
|
||||||
|
binding.profilePictureView.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
binding.profilePictureView.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
@ -289,7 +295,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
} else {
|
} else {
|
||||||
// if we have a network connection then attempt to update the display name
|
// if we have a network connection then attempt to update the display name
|
||||||
TextSecurePreferences.setProfileName(this, displayName)
|
TextSecurePreferences.setProfileName(this, displayName)
|
||||||
val user = configFactory.user
|
val user = viewModel.getUser()
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
|
Log.w(TAG, "Cannot update display name - missing user details from configFactory.")
|
||||||
} else {
|
} else {
|
||||||
@ -309,112 +315,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
return updateWasSuccessful
|
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
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
@ -605,20 +505,27 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
},
|
},
|
||||||
title = stringResource(R.string.profileDisplayPictureSet),
|
title = stringResource(R.string.profileDisplayPictureSet),
|
||||||
content = {
|
content = {
|
||||||
|
// custom content that has the displayed images
|
||||||
|
|
||||||
|
// main container that control the overall size and adds the rounded bg
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = LocalDimensions.current.smallSpacing)
|
.padding(top = LocalDimensions.current.smallSpacing)
|
||||||
|
|
||||||
.size(dimensionResource(id = R.dimen.large_profile_picture_size))
|
.size(dimensionResource(id = R.dimen.large_profile_picture_size))
|
||||||
.clickable {
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null // the ripple doesn't look nice as a square with the plus icon on top too
|
||||||
|
) {
|
||||||
startAvatarSelection()
|
startAvatarSelection()
|
||||||
}
|
}
|
||||||
|
.testTag(stringResource(R.string.AccessibilityId_avatarPicker))
|
||||||
.background(
|
.background(
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = LocalColors.current.backgroundBubbleReceived,
|
color = LocalColors.current.backgroundBubbleReceived,
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
// the image content will depend on state type
|
||||||
when(val s = state){
|
when(val s = state){
|
||||||
// user avatar
|
// user avatar
|
||||||
is UserAvatar -> {
|
is UserAvatar -> {
|
||||||
@ -646,6 +553,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// '+' button that sits atop the custom content
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(LocalDimensions.current.spacing)
|
.size(LocalDimensions.current.spacing)
|
||||||
@ -667,11 +575,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
DialogButtonModel(
|
DialogButtonModel(
|
||||||
text = GetString(R.string.save),
|
text = GetString(R.string.save),
|
||||||
contentDescription = GetString(R.string.AccessibilityId_save),
|
contentDescription = GetString(R.string.AccessibilityId_save),
|
||||||
|
enabled = state is TempAvatar,
|
||||||
onClick = saveAvatar
|
onClick = saveAvatar
|
||||||
),
|
),
|
||||||
DialogButtonModel(
|
DialogButtonModel(
|
||||||
text = GetString(R.string.remove),
|
text = GetString(R.string.remove),
|
||||||
contentDescription = GetString(R.string.AccessibilityId_remove),
|
contentDescription = GetString(R.string.AccessibilityId_remove),
|
||||||
|
enabled = state is UserAvatar || // can remove is the user has an avatar set
|
||||||
|
(state is TempAvatar && state.hasAvatar),
|
||||||
onClick = removeAvatar
|
onClick = removeAvatar
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,7 @@ package org.thoughtcrime.securesms.preferences
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.canhub.cropper.CropImage
|
import com.canhub.cropper.CropImage
|
||||||
import com.canhub.cropper.CropImageView
|
import com.canhub.cropper.CropImageView
|
||||||
@ -13,13 +11,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.libsession_util.util.UserPic
|
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.avatars.AvatarHelper
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
@ -32,6 +30,8 @@ import org.session.libsignal.utilities.ExternalStorageUtil.getImageDir
|
|||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.NoExternalStorageException
|
import org.session.libsignal.utilities.NoExternalStorageException
|
||||||
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
import org.session.libsignal.utilities.Util.SECURE_RANDOM
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
|
import org.thoughtcrime.securesms.preferences.SettingsViewModel.AvatarDialogState.TempAvatar
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||||
@ -44,7 +44,8 @@ import javax.inject.Inject
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsViewModel @Inject constructor(
|
class SettingsViewModel @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
val prefs: TextSecurePreferences
|
private val prefs: TextSecurePreferences,
|
||||||
|
private val configFactory: ConfigFactory
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val TAG = "SettingsViewModel"
|
private val TAG = "SettingsViewModel"
|
||||||
|
|
||||||
@ -52,12 +53,25 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
|
val hexEncodedPublicKey: String get() = prefs.getLocalNumber() ?: ""
|
||||||
|
|
||||||
|
private val userAddress = Address.fromSerialized(hexEncodedPublicKey)
|
||||||
|
|
||||||
private val _avatarDialogState: MutableStateFlow<AvatarDialogState> = MutableStateFlow(
|
private val _avatarDialogState: MutableStateFlow<AvatarDialogState> = MutableStateFlow(
|
||||||
getDefaultAvatarDialogState()
|
getDefaultAvatarDialogState()
|
||||||
)
|
)
|
||||||
val avatarDialogState: StateFlow<AvatarDialogState>
|
val avatarDialogState: StateFlow<AvatarDialogState>
|
||||||
get() = _avatarDialogState
|
get() = _avatarDialogState
|
||||||
|
|
||||||
|
private val _showLoader: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||||
|
val showLoader: StateFlow<Boolean>
|
||||||
|
get() = _showLoader
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the avatar on the main settings page
|
||||||
|
*/
|
||||||
|
private val _refreshAvatar: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||||
|
val refreshAvatar: SharedFlow<Unit>
|
||||||
|
get() = _refreshAvatar.asSharedFlow()
|
||||||
|
|
||||||
fun getDisplayName(): String =
|
fun getDisplayName(): String =
|
||||||
prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
|
prefs.getProfileName() ?: truncateIdForDisplay(hexEncodedPublicKey)
|
||||||
|
|
||||||
@ -77,6 +91,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun getTempFile() = tempFile
|
fun getTempFile() = tempFile
|
||||||
|
|
||||||
|
fun getUser() = configFactory.user
|
||||||
|
|
||||||
fun onAvatarPicked(result: CropImageView.CropResult) {
|
fun onAvatarPicked(result: CropImageView.CropResult) {
|
||||||
when {
|
when {
|
||||||
result.isSuccessful -> {
|
result.isSuccessful -> {
|
||||||
@ -93,7 +109,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
// update dialog with temporary avatar (has not been saved/uploaded yet)
|
// update dialog with temporary avatar (has not been saved/uploaded yet)
|
||||||
_avatarDialogState.value =
|
_avatarDialogState.value =
|
||||||
AvatarDialogState.TempAvatar(profilePictureToBeUploaded)
|
AvatarDialogState.TempAvatar(profilePictureToBeUploaded, hasAvatar())
|
||||||
} catch (e: BitmapDecodingException) {
|
} catch (e: BitmapDecodingException) {
|
||||||
Log.e(TAG, e)
|
Log.e(TAG, e)
|
||||||
}
|
}
|
||||||
@ -111,105 +127,115 @@ class SettingsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onAvatarDialogDismissed() {
|
fun onAvatarDialogDismissed() {
|
||||||
_avatarDialogState.value =getDefaultAvatarDialogState()
|
_avatarDialogState.value = getDefaultAvatarDialogState()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDefaultAvatarDialogState() = if (hasAvatar()) AvatarDialogState.UserAvatar(Address.fromSerialized(hexEncodedPublicKey))
|
fun getDefaultAvatarDialogState() = if (hasAvatar()) AvatarDialogState.UserAvatar(userAddress)
|
||||||
else AvatarDialogState.NoAvatar
|
else AvatarDialogState.NoAvatar
|
||||||
|
|
||||||
//todo properly close dialog when done and make sure the state is the right one post change
|
fun saveAvatar() {
|
||||||
//todo make ripple effect round in dialog avatar picker
|
val tempAvatar = (avatarDialogState.value as? TempAvatar)?.data
|
||||||
//todo link other states, like making sure we show the actual avatar if there's already one
|
?: return Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
//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() {
|
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(context);
|
||||||
object NoAvatar : AvatarDialogState()
|
if (!haveNetworkConnection) {
|
||||||
data class UserAvatar(val address: Address) : AvatarDialogState()
|
Log.w(TAG, "Cannot update profile picture - no network connection.")
|
||||||
data class TempAvatar(val data: ByteArray) : AvatarDialogState()
|
Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onFail: () -> Unit = {
|
||||||
|
Log.e(TAG, "Sync failed when uploading profile picture.")
|
||||||
|
Toast.makeText(context, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
syncProfilePicture(tempAvatar, onFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun removeAvatar() {
|
||||||
|
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(context);
|
||||||
|
if (!haveNetworkConnection) {
|
||||||
|
Log.w(TAG, "Cannot remove profile picture - no network connection.")
|
||||||
|
Toast.makeText(context, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val onFail: () -> Unit = {
|
||||||
|
Log.e(TAG, "Sync failed when removing profile picture.")
|
||||||
|
Toast.makeText(context, R.string.profileDisplayPictureRemoveError, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
val emptyProfilePicture = ByteArray(0)
|
||||||
|
syncProfilePicture(emptyProfilePicture, onFail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
|
// Helper method used by updateProfilePicture and removeProfilePicture to sync it online
|
||||||
/*private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
|
private fun syncProfilePicture(profilePicture: ByteArray, onFail: () -> Unit) {
|
||||||
binding.loader.isVisible = true
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_showLoader.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
// Grab the profile key and kick of the promise to update the profile picture
|
// Grab the profile key and kick of the promise to update the profile picture
|
||||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(context)
|
||||||
val updateProfilePicturePromise = ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)
|
ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, context)
|
||||||
|
|
||||||
// If the online portion of the update succeeded then update the local state
|
// If the online portion of the update succeeded then update the local state
|
||||||
updateProfilePicturePromise.successUi {
|
val userConfig = configFactory.user
|
||||||
|
AvatarHelper.setAvatar(
|
||||||
|
context,
|
||||||
|
Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)!!),
|
||||||
|
profilePicture
|
||||||
|
)
|
||||||
|
|
||||||
// When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
|
// When removing the profile picture the supplied ByteArray is empty so we'll clear the local data
|
||||||
if (profilePicture.isEmpty()) {
|
if (profilePicture.isEmpty()) {
|
||||||
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
MessagingModuleConfiguration.shared.storage.clearUserPic()
|
||||||
}
|
|
||||||
|
|
||||||
val userConfig = configFactory.user
|
// update dialog state
|
||||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
|
_avatarDialogState.value = AvatarDialogState.NoAvatar
|
||||||
prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() )
|
} else {
|
||||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
prefs.setProfileAvatarId(SECURE_RANDOM.nextInt())
|
||||||
|
ProfileKeyUtil.setEncodedProfileKey(context, encodedProfileKey)
|
||||||
|
|
||||||
// Attempt to grab the details we require to update the profile picture
|
// Attempt to grab the details we require to update the profile picture
|
||||||
val url = prefs.getProfilePictureURL()
|
val url = prefs.getProfilePictureURL()
|
||||||
val profileKey = ProfileKeyUtil.getProfileKey(this)
|
val profileKey = ProfileKeyUtil.getProfileKey(context)
|
||||||
|
|
||||||
// If we have a URL and a profile key then set the user's profile picture
|
// If we have a URL and a profile key then set the user's profile picture
|
||||||
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
if (!url.isNullOrEmpty() && profileKey.isNotEmpty()) {
|
||||||
userConfig?.setPic(UserPic(url, profileKey))
|
userConfig?.setPic(UserPic(url, profileKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update dialog state
|
||||||
|
_avatarDialogState.value = AvatarDialogState.UserAvatar(userAddress)
|
||||||
|
}
|
||||||
|
|
||||||
if (userConfig != null && userConfig.needsDump()) {
|
if (userConfig != null && userConfig.needsDump()) {
|
||||||
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
|
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(context)
|
||||||
|
} catch (e: Exception){ // If the sync failed then inform the user
|
||||||
// Update our visuals
|
Log.d(TAG, "Error syncing avatar: $e")
|
||||||
binding.profilePictureView.recycle()
|
withContext(Dispatchers.Main) {
|
||||||
binding.profilePictureView.update()
|
onFail()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the sync failed then inform the user
|
// Finally update the main avatar
|
||||||
updateProfilePicturePromise.failUi { onFail() }
|
_refreshAvatar.emit(Unit)
|
||||||
|
// And remove the loader animation after we've waited for the attempt to succeed or fail
|
||||||
// Finally, remove the loader animation after we've waited for the attempt to succeed or fail
|
_showLoader.value = false
|
||||||
updateProfilePicturePromise.alwaysUi { binding.loader.isVisible = false }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfilePicture(profilePicture: ByteArray) {
|
sealed class AvatarDialogState() {
|
||||||
|
object NoAvatar : AvatarDialogState()
|
||||||
val haveNetworkConnection = NetworkUtils.haveValidNetworkConnection(this@SettingsActivity);
|
data class UserAvatar(val address: Address) : AvatarDialogState()
|
||||||
if (!haveNetworkConnection) {
|
data class TempAvatar(
|
||||||
Log.w(TAG, "Cannot update profile picture - no network connection.")
|
val data: ByteArray,
|
||||||
Toast.makeText(this@SettingsActivity, R.string.profileErrorUpdate, Toast.LENGTH_LONG).show()
|
val hasAvatar: Boolean // true if the user has an avatar set already but is in this temp state because they are trying out a new avatar
|
||||||
return
|
) : AvatarDialogState()
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
@ -58,6 +58,7 @@ class DialogButtonModel(
|
|||||||
val contentDescription: GetString = text,
|
val contentDescription: GetString = text,
|
||||||
val color: Color = Color.Unspecified,
|
val color: Color = Color.Unspecified,
|
||||||
val dismissOnClick: Boolean = true,
|
val dismissOnClick: Boolean = true,
|
||||||
|
val enabled: Boolean = true,
|
||||||
val onClick: () -> Unit = {},
|
val onClick: () -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -164,7 +165,8 @@ fun AlertDialog(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.contentDescription(it.contentDescription())
|
.contentDescription(it.contentDescription())
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
color = it.color
|
color = it.color,
|
||||||
|
enabled = it.enabled
|
||||||
) {
|
) {
|
||||||
it.onClick()
|
it.onClick()
|
||||||
if (it.dismissOnClick) onDismissRequest()
|
if (it.dismissOnClick) onDismissRequest()
|
||||||
@ -222,16 +224,24 @@ fun DialogButton(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
color: Color = Color.Unspecified,
|
color: Color = Color.Unspecified,
|
||||||
|
enabled: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
|
enabled = enabled,
|
||||||
onClick = onClick
|
onClick = onClick
|
||||||
) {
|
) {
|
||||||
|
val textColor = if(enabled) {
|
||||||
|
color.takeOrElse { LocalColors.current.text }
|
||||||
|
} else {
|
||||||
|
LocalColors.current.disabled
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
color = color.takeOrElse { LocalColors.current.text },
|
color = textColor,
|
||||||
style = LocalType.current.large.bold(),
|
style = LocalType.current.large.bold(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
|
@ -1,45 +1,111 @@
|
|||||||
package org.session.libsession.utilities
|
package org.session.libsession.utilities
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import nl.komponents.kovenant.Promise
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import nl.komponents.kovenant.deferred
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
|
import org.session.libsession.avatars.AvatarHelper
|
||||||
import org.session.libsession.messaging.file_server.FileServerApi
|
import org.session.libsession.messaging.file_server.FileServerApi
|
||||||
import org.session.libsignal.streams.ProfileCipherOutputStream
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
import org.session.libsignal.utilities.ProfileAvatarData
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLastProfilePictureUpload
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.getProfileKey
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences.Companion.setLastProfilePictureUpload
|
||||||
import org.session.libsignal.streams.DigestingRequestBody
|
import org.session.libsignal.streams.DigestingRequestBody
|
||||||
|
import org.session.libsignal.streams.ProfileCipherOutputStream
|
||||||
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
import org.session.libsignal.streams.ProfileCipherOutputStreamFactory
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.session.libsignal.utilities.ProfileAvatarData
|
||||||
|
import org.session.libsignal.utilities.ThreadUtils.queue
|
||||||
import org.session.libsignal.utilities.retryIfNeeded
|
import org.session.libsignal.utilities.retryIfNeeded
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object ProfilePictureUtilities {
|
object ProfilePictureUtilities {
|
||||||
|
|
||||||
fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): Promise<Unit, Exception> {
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
val deferred = deferred<Unit, Exception>()
|
fun resubmitProfilePictureIfNeeded(context: Context) {
|
||||||
ThreadUtils.queue {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
// Files expire on the file server after a while, so we simply re-upload the user's profile picture
|
||||||
|
// at a certain interval to ensure it's always available.
|
||||||
|
val userPublicKey = getLocalNumber(context) ?: return@launch
|
||||||
|
val now = Date().time
|
||||||
|
val lastProfilePictureUpload = getLastProfilePictureUpload(context)
|
||||||
|
if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return@launch
|
||||||
|
|
||||||
|
// Don't generate a new profile key here; we do that when the user changes their profile picture
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Started")
|
||||||
|
val encodedProfileKey =
|
||||||
|
getProfileKey(context)
|
||||||
|
try {
|
||||||
|
// Read the file into a byte array
|
||||||
|
val inputStream = AvatarHelper.getInputStreamFor(
|
||||||
|
context,
|
||||||
|
fromSerialized(userPublicKey)
|
||||||
|
)
|
||||||
|
val baos = ByteArrayOutputStream()
|
||||||
|
var count: Int
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
while ((inputStream.read(buffer, 0, buffer.size)
|
||||||
|
.also { count = it }) != -1
|
||||||
|
) {
|
||||||
|
baos.write(buffer, 0, count)
|
||||||
|
}
|
||||||
|
baos.flush()
|
||||||
|
val profilePicture = baos.toByteArray()
|
||||||
|
// Re-upload it
|
||||||
|
upload(
|
||||||
|
profilePicture,
|
||||||
|
encodedProfileKey!!,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the last profile picture upload date
|
||||||
|
setLastProfilePictureUpload(
|
||||||
|
context,
|
||||||
|
Date().time
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.d("Loki-Avatar", "Uploading Avatar Finished")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Loki-Avatar", "Uploading avatar failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context) {
|
||||||
val inputStream = ByteArrayInputStream(profilePicture)
|
val inputStream = ByteArrayInputStream(profilePicture)
|
||||||
val outputStream = ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong())
|
val outputStream =
|
||||||
|
ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong())
|
||||||
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
||||||
val pad = ProfileAvatarData(inputStream, outputStream, "image/jpeg", ProfileCipherOutputStreamFactory(profileKey))
|
val pad = ProfileAvatarData(
|
||||||
val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, pad.contentType, pad.dataLength, null)
|
inputStream,
|
||||||
|
outputStream,
|
||||||
|
"image/jpeg",
|
||||||
|
ProfileCipherOutputStreamFactory(profileKey)
|
||||||
|
)
|
||||||
|
val drb = DigestingRequestBody(
|
||||||
|
pad.data,
|
||||||
|
pad.outputStreamFactory,
|
||||||
|
pad.contentType,
|
||||||
|
pad.dataLength,
|
||||||
|
null
|
||||||
|
)
|
||||||
val b = Buffer()
|
val b = Buffer()
|
||||||
drb.writeTo(b)
|
drb.writeTo(b)
|
||||||
val data = b.readByteArray()
|
val data = b.readByteArray()
|
||||||
var id: Long = 0
|
var id: Long = 0
|
||||||
try {
|
|
||||||
|
// this can throw an error
|
||||||
id = retryIfNeeded(4) {
|
id = retryIfNeeded(4) {
|
||||||
FileServerApi.upload(data)
|
FileServerApi.upload(data)
|
||||||
}.get()
|
}.get()
|
||||||
} catch (e: Exception) {
|
|
||||||
deferred.reject(e)
|
|
||||||
}
|
|
||||||
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
|
||||||
val url = "${FileServerApi.server}/file/$id"
|
val url = "${FileServerApi.server}/file/$id"
|
||||||
TextSecurePreferences.setProfilePictureURL(context, url)
|
TextSecurePreferences.setProfilePictureURL(context, url)
|
||||||
deferred.resolve(Unit)
|
|
||||||
}
|
|
||||||
return deferred.promise
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user