mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
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:
committed by
Greyson Parrelli
parent
a75f634c0a
commit
a27d60f830
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user