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
Alex Hart 2021-07-21 13:35:47 -03:00 zatwierdzone przez Greyson Parrelli
rodzic a75f634c0a
commit a27d60f830
20 zmienionych plików z 293 dodań i 119 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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,15 +95,26 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
photoButton.setOnIconClickedListener { openGallery() }
textButton.setOnIconClickedListener { openTextEditor(null) }
saveButton.setOnClickListener { v ->
viewModel.save {
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putParcelable(SELECT_AVATAR_MEDIA, it)
}
)
Navigation.findNavController(v).popBackStack()
}
viewModel.save(
{
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putParcelable(SELECT_AVATAR_MEDIA, it)
}
)
Navigation.findNavController(v).popBackStack()
},
{
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putBoolean(SELECT_AVATAR_CLEAR, true)
}
)
Navigation.findNavController(v).popBackStack()
}
)
}
clearButton.setOnClickListener { viewModel.clear() }

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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) {
val avatar = store.state.currentAvatar ?: throw AssertionError()
persistAndCreateMedia(avatar, onSaved)
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)

Wyświetl plik

@ -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))
if (!hadText) {
textInput.setSelection(textInput.length())
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())
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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) -> {
Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
handleMediaFromResult(media);
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);
}
});
}

Wyświetl plik

@ -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) -> {
Media result = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA);
viewModel.onAvatarSelected(requireContext(), result);
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);
avatarPlaceholderView.setVisibility(View.VISIBLE);
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);

Wyświetl plik

@ -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,26 +33,28 @@ class ManageProfileViewModel extends ViewModel {
private static final String TAG = Log.tag(ManageProfileViewModel.class);
private final MutableLiveData<AvatarState> avatar;
private final MutableLiveData<ProfileName> profileName;
private final MutableLiveData<String> username;
private final MutableLiveData<String> about;
private final MutableLiveData<String> aboutEmoji;
private final SingleLiveEvent<Event> events;
private final RecipientForeverObserver observer;
private final ManageProfileRepository repository;
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;
private byte[] previousAvatar;
public ManageProfileViewModel() {
this.avatar = new MutableLiveData<>();
this.profileName = new MutableLiveData<>();
this.username = new MutableLiveData<>();
this.about = new MutableLiveData<>();
this.aboutEmoji = new MutableLiveData<>();
this.events = new SingleLiveEvent<>();
this.repository = new ManageProfileRepository();
this.observer = this::onRecipientChanged;
this.internalAvatarState = new MutableLiveData<>();
this.profileName = new MutableLiveData<>();
this.username = new MutableLiveData<>();
this.about = new MutableLiveData<>();
this.aboutEmoji = new MutableLiveData<>();
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() {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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