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.
This commit is contained in:
Alex Hart
2021-07-21 13:35:47 -03:00
committed by Greyson Parrelli
parent a75f634c0a
commit a27d60f830
20 changed files with 293 additions and 119 deletions

View File

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

View File

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

View File

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