kopia lustrzana https://github.com/ryukoposting/Signal-Android
Adjust new avatar picker logic.
* Better emoji rendering support * Deleting an avatar will deselect it * Added padding to the bottom of recyclers * Disabled save if no edit / selection has been made. * Clearing and saving will remove a user's avatar.fork-5.53.8
rodzic
a75f634c0a
commit
a27d60f830
|
@ -24,8 +24,8 @@ jobs:
|
|||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Remove Android S
|
||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-S"
|
||||
- name: Remove Android 31 (S)
|
||||
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-31"
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa
|
||||
|
|
|
@ -3,13 +3,11 @@ package org.thoughtcrime.securesms.avatar
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
|
@ -28,7 +26,7 @@ import javax.annotation.meta.Exhaustive
|
|||
*/
|
||||
object AvatarRenderer {
|
||||
|
||||
private val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
|
||||
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
|
||||
|
||||
fun getTypeface(context: Context): Typeface {
|
||||
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
|
||||
|
@ -50,30 +48,8 @@ object AvatarRenderer {
|
|||
avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
size: Int = DIMENSIONS,
|
||||
isRect: Boolean = true
|
||||
): Drawable {
|
||||
val typeface = getTypeface(context)
|
||||
val color: Int = if (inverted) {
|
||||
avatar.color.backgroundColor
|
||||
} else {
|
||||
avatar.color.foregroundColor
|
||||
}
|
||||
|
||||
val builder = TextDrawable
|
||||
.builder()
|
||||
.beginConfig()
|
||||
.fontSize(Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f).toInt())
|
||||
.textColor(color)
|
||||
.useFont(typeface)
|
||||
.width(size)
|
||||
.height(size)
|
||||
.endConfig()
|
||||
|
||||
return if (isRect) {
|
||||
builder.buildRect(avatar.text, Color.TRANSPARENT)
|
||||
} else {
|
||||
builder.buildRound(avatar.text, Color.TRANSPARENT)
|
||||
}
|
||||
return TextAvatarDrawable(context, avatar, inverted, size)
|
||||
}
|
||||
|
||||
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
|
||||
/**
|
||||
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
|
||||
*/
|
||||
class TextAvatarDrawable(
|
||||
context: Context,
|
||||
avatar: Avatar.Text,
|
||||
inverted: Boolean = false,
|
||||
private val size: Int = AvatarRenderer.DIMENSIONS,
|
||||
) : Drawable() {
|
||||
|
||||
private val layout: FrameLayout = FrameLayout(context)
|
||||
private val textView: EmojiTextView = EmojiTextView(context)
|
||||
|
||||
init {
|
||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
|
||||
textView.text = avatar.text
|
||||
textView.gravity = Gravity.CENTER
|
||||
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
|
||||
|
||||
layout.addView(textView)
|
||||
|
||||
textView.updateLayoutParams {
|
||||
width = size
|
||||
height = size
|
||||
}
|
||||
|
||||
layout.measure(size, size)
|
||||
layout.layout(0, 0, size, size)
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int = size
|
||||
|
||||
override fun getIntrinsicWidth(): Int = size
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
layout.draw(canvas)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
}
|
|
@ -40,6 +40,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||
companion object {
|
||||
const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR"
|
||||
const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA"
|
||||
const val SELECT_AVATAR_CLEAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_CLEAR"
|
||||
|
||||
private const val REQUEST_CODE_SELECT_IMAGE = 1
|
||||
}
|
||||
|
@ -94,7 +95,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||
photoButton.setOnIconClickedListener { openGallery() }
|
||||
textButton.setOnIconClickedListener { openTextEditor(null) }
|
||||
saveButton.setOnClickListener { v ->
|
||||
viewModel.save {
|
||||
viewModel.save(
|
||||
{
|
||||
setFragmentResult(
|
||||
REQUEST_KEY_SELECT_AVATAR,
|
||||
Bundle().apply {
|
||||
|
@ -102,7 +104,17 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
|||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
},
|
||||
{
|
||||
setFragmentResult(
|
||||
REQUEST_KEY_SELECT_AVATAR,
|
||||
Bundle().apply {
|
||||
putBoolean(SELECT_AVATAR_CLEAR, true)
|
||||
}
|
||||
)
|
||||
Navigation.findNavController(v).popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
clearButton.setOnClickListener { viewModel.clear() }
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.widget.ImageView
|
|||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
|
@ -58,17 +57,14 @@ object AvatarPickerItem {
|
|||
init {
|
||||
textView.typeface = AvatarRenderer.getTypeface(context)
|
||||
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateTextSize()
|
||||
updateAndApplyText(textView.text.toString())
|
||||
}
|
||||
textView.addTextChangedListener(
|
||||
afterTextChanged = {
|
||||
updateTextSize()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateTextSize() {
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, textView.text.toString(), textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f))
|
||||
private fun updateAndApplyText(text: String) {
|
||||
val textSize = Avatars.getTextSizeForLength(context, text, textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)
|
||||
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
textView.text = text
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
|
@ -112,9 +108,7 @@ object AvatarPickerItem {
|
|||
is Avatar.Text -> {
|
||||
textView.visible = true
|
||||
|
||||
if (textView.text.toString() != model.avatar.text) {
|
||||
textView.text = model.avatar.text
|
||||
}
|
||||
updateAndApplyText(model.avatar.text)
|
||||
|
||||
imageView.setImageDrawable(null)
|
||||
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.avatar.Avatar
|
|||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
|
@ -61,10 +62,10 @@ class AvatarPickerRepository(context: Context) {
|
|||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to read group avatar!")
|
||||
getDefaultAvatarForGroup()
|
||||
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
} else {
|
||||
getDefaultAvatarForGroup()
|
||||
getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,12 +156,25 @@ class AvatarPickerRepository(context: Context) {
|
|||
return if (initials.isNullOrBlank()) {
|
||||
Avatar.getDefaultForSelf()
|
||||
} else {
|
||||
Avatar.Text(initials, Avatars.colors.random(), Avatar.DatabaseId.NotSet)
|
||||
Avatar.Text(initials, requireNotNull(Avatars.colorMap[Recipient.self().avatarColor.serialize()]), Avatar.DatabaseId.DoNotPersist)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultAvatarForGroup(): Avatar {
|
||||
return Avatar.getDefaultForGroup()
|
||||
fun getDefaultAvatarForGroup(groupId: GroupId): Avatar {
|
||||
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
|
||||
|
||||
return getDefaultAvatarForGroup(recipient.avatarColor)
|
||||
}
|
||||
|
||||
fun getDefaultAvatarForGroup(color: AvatarColor?): Avatar {
|
||||
val colorPair = Avatars.colorMap[color?.serialize()]
|
||||
val defaultColor = Avatar.getDefaultForGroup()
|
||||
|
||||
return if (colorPair != null) {
|
||||
defaultColor.copy(color = colorPair)
|
||||
} else {
|
||||
defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||
|
|
|
@ -6,5 +6,6 @@ data class AvatarPickerState(
|
|||
val currentAvatar: Avatar? = null,
|
||||
val selectableAvatars: List<Avatar> = listOf(),
|
||||
val canSave: Boolean = false,
|
||||
val canClear: Boolean = false
|
||||
val canClear: Boolean = false,
|
||||
val isCleared: Boolean = false
|
||||
)
|
||||
|
|
|
@ -27,6 +27,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
|
||||
fun delete(avatar: Avatar) {
|
||||
repository.delete(avatar) {
|
||||
refreshAvatar()
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
|
@ -34,22 +35,26 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
fun clear() {
|
||||
store.update {
|
||||
val avatar = getDefaultAvatarFromRepository()
|
||||
it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = false)
|
||||
it.copy(currentAvatar = avatar, canSave = true, canClear = false, isCleared = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun save(onSaved: (Media) -> Unit) {
|
||||
fun save(onSaved: (Media) -> Unit, onCleared: () -> Unit) {
|
||||
if (store.state.isCleared) {
|
||||
onCleared()
|
||||
} else {
|
||||
val avatar = store.state.currentAvatar ?: throw AssertionError()
|
||||
persistAndCreateMedia(avatar, onSaved)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAvatarSelectedFromGrid(avatar: Avatar) {
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) }
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||
}
|
||||
|
||||
fun onAvatarEditCompleted(avatar: Avatar) {
|
||||
persistAvatar(avatar) { saved ->
|
||||
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true) }
|
||||
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true, isCleared = false) }
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +62,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
fun onAvatarPhotoSelectionCompleted(media: Media) {
|
||||
repository.writeMediaToMultiSessionStorage(media) { multiSessionUri ->
|
||||
persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar ->
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) }
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
|
||||
refreshSelectableAvatars()
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +71,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
protected fun refreshAvatar() {
|
||||
disposables.add(
|
||||
getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar ->
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = !isSaveable(avatar)) }
|
||||
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = avatar is Avatar.Photo && !isSaveable(avatar), isCleared = false) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -84,7 +89,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
)
|
||||
}
|
||||
|
||||
private fun isSaveable(avatar: Avatar) = !(avatar is Avatar.Photo && avatar.databaseId == Avatar.DatabaseId.DoNotPersist)
|
||||
private fun isSaveable(avatar: Avatar) = avatar.databaseId != Avatar.DatabaseId.DoNotPersist
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
|
@ -132,7 +137,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
}
|
||||
}
|
||||
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup()
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(groupId)
|
||||
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForGroup(groupId)
|
||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||
|
||||
|
@ -161,11 +166,11 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
|
|||
return if (initialAvatar != null) {
|
||||
Single.just(initialAvatar)
|
||||
} else {
|
||||
Single.just(getDefaultAvatarFromRepository())
|
||||
Single.fromCallable { getDefaultAvatarFromRepository() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup()
|
||||
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(null)
|
||||
override fun getPersistedAvatars(): Single<List<Avatar>> = Single.just(listOf())
|
||||
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
|
||||
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar)
|
||||
|
|
|
@ -41,6 +41,8 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
|||
private val withRecyclerSet = ConstraintSet()
|
||||
private val withoutRecyclerSet = ConstraintSet()
|
||||
|
||||
private var hasBoundFromViewModel: Boolean = false
|
||||
|
||||
private fun createFactory(): TextAvatarCreationViewModel.Factory {
|
||||
val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments())
|
||||
val textBundle = args.textAvatar
|
||||
|
@ -83,17 +85,25 @@ class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragme
|
|||
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
|
||||
|
||||
val hadText = textInput.length() > 0
|
||||
val selectionStart = textInput.selectionStart
|
||||
val selectionEnd = textInput.selectionEnd
|
||||
|
||||
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
|
||||
textInput.post {
|
||||
if (!hadText) {
|
||||
textInput.setSelection(textInput.length())
|
||||
} else {
|
||||
textInput.setSelection(selectionStart, selectionEnd)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
|
||||
hasBoundFromViewModel = true
|
||||
}
|
||||
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3)
|
||||
textInput.doAfterTextChanged {
|
||||
if (it != null) {
|
||||
if (it != null && hasBoundFromViewModel) {
|
||||
viewModel.setText(it.toString())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.thoughtcrime.securesms.avatar.text
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
|
@ -11,14 +12,20 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
|
|||
|
||||
private val store = Store(TextAvatarCreationState(initialText))
|
||||
|
||||
val state: LiveData<TextAvatarCreationState> = store.stateLiveData
|
||||
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
|
||||
|
||||
fun setColor(colorPair: Avatars.ColorPair) {
|
||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
||||
}
|
||||
|
||||
fun setText(text: String) {
|
||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(text = text)) }
|
||||
store.update {
|
||||
if (it.currentAvatar.text == text) {
|
||||
it
|
||||
} else {
|
||||
it.copy(currentAvatar = it.currentAvatar.copy(text = text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentAvatar(): Avatar.Text {
|
||||
|
|
|
@ -54,7 +54,7 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
|||
if (!TextUtils.isEmpty(character)) {
|
||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color);
|
||||
Avatar.Text avatar = new Avatar.Text(character, new Avatars.ColorPair(color, foregroundColor), Avatar.DatabaseId.DoNotPersist.INSTANCE);
|
||||
Drawable foreground = AvatarRenderer.createTextDrawable(context, avatar, inverted, targetSize, false);
|
||||
Drawable foreground = AvatarRenderer.createTextDrawable(context, avatar, inverted, targetSize);
|
||||
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
|
||||
|
||||
background.setColorFilter(new SimpleColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt()));
|
||||
|
|
|
@ -170,6 +170,12 @@ public class AddGroupDetailsFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
private void handleMediaResult(Bundle data) {
|
||||
if (data.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||
viewModel.setAvatarMedia(null);
|
||||
viewModel.setAvatar(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final Media result = data.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||
final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri());
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ import org.thoughtcrime.securesms.LoggingFragment;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
|
@ -104,8 +106,14 @@ public class EditProfileFragment extends LoggingFragment {
|
|||
initializeProfileName();
|
||||
|
||||
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
||||
if (bundle.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||
viewModel.setAvatarMedia(null);
|
||||
viewModel.setAvatar(null);
|
||||
avatar.setImageDrawable(null);
|
||||
} else {
|
||||
Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||
handleMediaFromResult(media);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package org.thoughtcrime.securesms.profiles.manage;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -18,28 +19,28 @@ import androidx.core.content.res.ResourcesCompat;
|
|||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.Avatars;
|
||||
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState;
|
||||
import org.thoughtcrime.securesms.util.NameUtil;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
public class ManageProfileFragment extends LoggingFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ManageProfileFragment.class);
|
||||
|
||||
private Toolbar toolbar;
|
||||
private ImageView avatarView;
|
||||
private View avatarPlaceholderView;
|
||||
private ImageView avatarPlaceholderView;
|
||||
private TextView profileNameView;
|
||||
private View profileNameContainer;
|
||||
private TextView usernameView;
|
||||
|
@ -48,6 +49,8 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
private View aboutContainer;
|
||||
private ImageView aboutEmojiView;
|
||||
private AlertDialog avatarProgress;
|
||||
private TextView avatarInitials;
|
||||
private ImageView avatarBackground;
|
||||
|
||||
private ManageProfileViewModel viewModel;
|
||||
|
||||
|
@ -68,6 +71,8 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
this.aboutView = view.findViewById(R.id.manage_profile_about);
|
||||
this.aboutContainer = view.findViewById(R.id.manage_profile_about_container);
|
||||
this.aboutEmojiView = view.findViewById(R.id.manage_profile_about_icon);
|
||||
this.avatarInitials = view.findViewById(R.id.manage_profile_avatar_initials);
|
||||
this.avatarBackground = view.findViewById(R.id.manage_profile_avatar_background);
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
|
@ -87,8 +92,18 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
});
|
||||
|
||||
getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> {
|
||||
if (bundle.getBoolean(AvatarPickerFragment.SELECT_AVATAR_CLEAR)) {
|
||||
viewModel.onAvatarSelected(requireContext(), null);
|
||||
} else {
|
||||
Media result = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
|
||||
viewModel.onAvatarSelected(requireContext(), result);
|
||||
}
|
||||
});
|
||||
|
||||
avatarInitials.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
if (avatarInitials.length() > 0) {
|
||||
updateInitials(avatarInitials.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -111,9 +126,26 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
private void presentAvatar(@NonNull AvatarState avatarState) {
|
||||
if (avatarState.getAvatar() == null) {
|
||||
avatarView.setImageDrawable(null);
|
||||
|
||||
CharSequence initials = NameUtil.getAbbreviation(avatarState.getSelf().getDisplayName(requireContext()));
|
||||
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(avatarState.getSelf().getAvatarColor());
|
||||
|
||||
avatarBackground.setColorFilter(new SimpleColorFilter(avatarState.getSelf().getAvatarColor().colorInt()));
|
||||
avatarPlaceholderView.setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt()));
|
||||
avatarInitials.setTextColor(foregroundColor.getColorInt());
|
||||
|
||||
if (TextUtils.isEmpty(initials)) {
|
||||
avatarPlaceholderView.setVisibility(View.VISIBLE);
|
||||
avatarInitials.setVisibility(View.GONE);
|
||||
} else {
|
||||
updateInitials(initials.toString());
|
||||
avatarPlaceholderView.setVisibility(View.GONE);
|
||||
avatarInitials.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
avatarPlaceholderView.setVisibility(View.GONE);
|
||||
avatarInitials.setVisibility(View.GONE);
|
||||
|
||||
Glide.with(this)
|
||||
.load(avatarState.getAvatar())
|
||||
.circleCrop()
|
||||
|
@ -127,6 +159,11 @@ public class ManageProfileFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateInitials(String initials) {
|
||||
avatarInitials.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(requireContext(), initials, avatarInitials.getMeasuredWidth() * 0.8f, avatarInitials.getMeasuredWidth() * 0.45f));
|
||||
avatarInitials.setText(initials);
|
||||
}
|
||||
|
||||
private void presentProfileName(@Nullable ProfileName profileName) {
|
||||
if (profileName == null || profileName.isEmpty()) {
|
||||
profileNameView.setText(R.string.ManageProfileFragment_profile_name);
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -32,11 +33,12 @@ class ManageProfileViewModel extends ViewModel {
|
|||
|
||||
private static final String TAG = Log.tag(ManageProfileViewModel.class);
|
||||
|
||||
private final MutableLiveData<AvatarState> avatar;
|
||||
private final MutableLiveData<InternalAvatarState> internalAvatarState;
|
||||
private final MutableLiveData<ProfileName> profileName;
|
||||
private final MutableLiveData<String> username;
|
||||
private final MutableLiveData<String> about;
|
||||
private final MutableLiveData<String> aboutEmoji;
|
||||
private final LiveData<AvatarState> avatarState;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final RecipientForeverObserver observer;
|
||||
private final ManageProfileRepository repository;
|
||||
|
@ -44,7 +46,7 @@ class ManageProfileViewModel extends ViewModel {
|
|||
private byte[] previousAvatar;
|
||||
|
||||
public ManageProfileViewModel() {
|
||||
this.avatar = new MutableLiveData<>();
|
||||
this.internalAvatarState = new MutableLiveData<>();
|
||||
this.profileName = new MutableLiveData<>();
|
||||
this.username = new MutableLiveData<>();
|
||||
this.about = new MutableLiveData<>();
|
||||
|
@ -52,6 +54,7 @@ class ManageProfileViewModel extends ViewModel {
|
|||
this.events = new SingleLiveEvent<>();
|
||||
this.repository = new ManageProfileRepository();
|
||||
this.observer = this::onRecipientChanged;
|
||||
this.avatarState = LiveDataUtil.combineLatest(Recipient.self().live().getLiveData(), internalAvatarState, (self, state) -> new AvatarState(state, self));
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
onRecipientChanged(Recipient.self().fresh());
|
||||
|
@ -59,13 +62,13 @@ class ManageProfileViewModel extends ViewModel {
|
|||
StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
|
||||
if (details != null) {
|
||||
try {
|
||||
avatar.postValue(AvatarState.loaded(StreamUtil.readFully(details.getStream())));
|
||||
internalAvatarState.postValue(InternalAvatarState.loaded(StreamUtil.readFully(details.getStream())));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read avatar!");
|
||||
avatar.postValue(AvatarState.none());
|
||||
internalAvatarState.postValue(InternalAvatarState.none());
|
||||
}
|
||||
} else {
|
||||
avatar.postValue(AvatarState.none());
|
||||
internalAvatarState.postValue(InternalAvatarState.none());
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
|
||||
|
@ -75,7 +78,7 @@ class ManageProfileViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public @NonNull LiveData<AvatarState> getAvatar() {
|
||||
return avatar;
|
||||
return avatarState;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<ProfileName> getProfileName() {
|
||||
|
@ -103,18 +106,18 @@ class ManageProfileViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public void onAvatarSelected(@NonNull Context context, @Nullable Media media) {
|
||||
previousAvatar = avatar.getValue() != null ? avatar.getValue().getAvatar() : null;
|
||||
previousAvatar = internalAvatarState.getValue() != null ? internalAvatarState.getValue().getAvatar() : null;
|
||||
|
||||
if (media == null) {
|
||||
avatar.postValue(AvatarState.loading(null));
|
||||
internalAvatarState.postValue(InternalAvatarState.loading(null));
|
||||
repository.clearAvatar(context, result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
avatar.postValue(AvatarState.loaded(null));
|
||||
internalAvatarState.postValue(InternalAvatarState.loaded(null));
|
||||
previousAvatar = null;
|
||||
break;
|
||||
case FAILURE_NETWORK:
|
||||
avatar.postValue(AvatarState.loaded(previousAvatar));
|
||||
internalAvatarState.postValue(InternalAvatarState.loaded(previousAvatar));
|
||||
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
|
@ -125,16 +128,16 @@ class ManageProfileViewModel extends ViewModel {
|
|||
InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
|
||||
byte[] data = StreamUtil.readFully(stream);
|
||||
|
||||
avatar.postValue(AvatarState.loading(data));
|
||||
internalAvatarState.postValue(InternalAvatarState.loading(data));
|
||||
|
||||
repository.setAvatar(context, data, media.getMimeType(), result -> {
|
||||
switch (result) {
|
||||
case SUCCESS:
|
||||
avatar.postValue(AvatarState.loaded(data));
|
||||
internalAvatarState.postValue(InternalAvatarState.loaded(data));
|
||||
previousAvatar = data;
|
||||
break;
|
||||
case FAILURE_NETWORK:
|
||||
avatar.postValue(AvatarState.loaded(previousAvatar));
|
||||
internalAvatarState.postValue(InternalAvatarState.loaded(previousAvatar));
|
||||
events.postValue(Event.AVATAR_NETWORK_FAILURE);
|
||||
break;
|
||||
}
|
||||
|
@ -148,7 +151,7 @@ class ManageProfileViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public boolean canRemoveAvatar() {
|
||||
return avatar.getValue() != null;
|
||||
return internalAvatarState.getValue() != null;
|
||||
}
|
||||
|
||||
private void onRecipientChanged(@NonNull Recipient recipient) {
|
||||
|
@ -163,25 +166,49 @@ class ManageProfileViewModel extends ViewModel {
|
|||
Recipient.self().live().removeForeverObserver(observer);
|
||||
}
|
||||
|
||||
public static class AvatarState {
|
||||
public final static class AvatarState {
|
||||
private final InternalAvatarState internalAvatarState;
|
||||
private final Recipient self;
|
||||
|
||||
public AvatarState(@NonNull InternalAvatarState internalAvatarState,
|
||||
@NonNull Recipient self)
|
||||
{
|
||||
this.internalAvatarState = internalAvatarState;
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
public @Nullable byte[] getAvatar() {
|
||||
return internalAvatarState.avatar;
|
||||
}
|
||||
|
||||
public @NonNull LoadingState getLoadingState() {
|
||||
return internalAvatarState.loadingState;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSelf() {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
private final static class InternalAvatarState {
|
||||
private final byte[] avatar;
|
||||
private final LoadingState loadingState;
|
||||
|
||||
public AvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
|
||||
public InternalAvatarState(@Nullable byte[] avatar, @NonNull LoadingState loadingState) {
|
||||
this.avatar = avatar;
|
||||
this.loadingState = loadingState;
|
||||
}
|
||||
|
||||
private static @NonNull AvatarState none() {
|
||||
return new AvatarState(null, LoadingState.LOADED);
|
||||
private static @NonNull InternalAvatarState none() {
|
||||
return new InternalAvatarState(null, LoadingState.LOADED);
|
||||
}
|
||||
|
||||
private static @NonNull AvatarState loaded(@Nullable byte[] avatar) {
|
||||
return new AvatarState(avatar, LoadingState.LOADED);
|
||||
private static @NonNull InternalAvatarState loaded(@Nullable byte[] avatar) {
|
||||
return new InternalAvatarState(avatar, LoadingState.LOADED);
|
||||
}
|
||||
|
||||
private static @NonNull AvatarState loading(@Nullable byte[] avatar) {
|
||||
return new AvatarState(avatar, LoadingState.LOADING);
|
||||
private static @NonNull InternalAvatarState loading(@Nullable byte[] avatar) {
|
||||
return new InternalAvatarState(avatar, LoadingState.LOADING);
|
||||
}
|
||||
|
||||
public @Nullable byte[] getAvatar() {
|
||||
|
|
|
@ -111,6 +111,7 @@
|
|||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -46,6 +46,21 @@
|
|||
app:layout_constraintTop_toTopOf="@+id/manage_profile_avatar_background"
|
||||
app:srcCompat="@drawable/ic_profile_outline_40" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/manage_profile_avatar_initials"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/manage_profile_avatar_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/manage_profile_avatar_background"
|
||||
app:layout_constraintStart_toStartOf="@id/manage_profile_avatar_background"
|
||||
app:layout_constraintTop_toTopOf="@id/manage_profile_avatar_background"
|
||||
tools:ignore="SpUsage"
|
||||
tools:text="AF"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/manage_profile_avatar"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="80dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
Ładowanie…
Reference in New Issue