Fix responsiveness of profile photo edit UI.

There were various issues around the profile photo updating correctly in
the edit view. We want to make sure that what the user sees there is
what other people are seeing.

So I made some changes to make sure that when you remove your profile
photo the UI updates right away, as well as fixed most flickering
issues.
fork-5.53.8
Greyson Parrelli 2022-03-11 18:35:43 -05:00 zatwierdzone przez Cody Henthorne
rodzic e7a370a549
commit 78de70881f
6 zmienionych plików z 70 dodań i 36 usunięć

Wyświetl plik

@ -81,19 +81,22 @@ public class RetrieveProfileAvatarJob extends BaseJob {
return; return;
} }
if (Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) { if (profileAvatar != null && profileAvatar.equals(recipient.resolve().getProfileAvatar())) {
Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar); Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar);
return; return;
} }
if (TextUtils.isEmpty(profileAvatar)) { if (TextUtils.isEmpty(profileAvatar)) {
Log.w(TAG, "Removing profile avatar (no url) for: " + recipient.getId().serialize()); if (AvatarHelper.hasAvatar(context, recipient.getId())) {
AvatarHelper.delete(context, recipient.getId()); Log.w(TAG, "Removing profile avatar (no url) for: " + recipient.getId().serialize());
database.setProfileAvatar(recipient.getId(), profileAvatar); AvatarHelper.delete(context, recipient.getId());
database.setProfileAvatar(recipient.getId(), profileAvatar);
}
return; return;
} }
File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir()); File downloadDestination = File.createTempFile("avatar", "jpg", context.getCacheDir());
try { try {
SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver();

Wyświetl plik

@ -83,6 +83,7 @@ public class AvatarHelper {
*/ */
public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) { public static void delete(@NonNull Context context, @NonNull RecipientId recipientId) {
getAvatarFile(context, recipientId).delete(); getAvatarFile(context, recipientId).delete();
Recipient.live(recipientId).refresh();
} }
/** /**

Wyświetl plik

@ -39,10 +39,13 @@ import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarS
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.NameUtil; import org.thoughtcrime.securesms.util.NameUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation; import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
public class ManageProfileFragment extends LoggingFragment { public class ManageProfileFragment extends LoggingFragment {
private static final String TAG = Log.tag(ManageProfileFragment.class); private static final String TAG = Log.tag(ManageProfileFragment.class);
@ -137,7 +140,8 @@ public class ManageProfileFragment extends LoggingFragment {
private void initializeViewModel() { private void initializeViewModel() {
viewModel = ViewModelProviders.of(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class); viewModel = ViewModelProviders.of(this, new ManageProfileViewModel.Factory()).get(ManageProfileViewModel.class);
LiveData<Optional<byte[]>> avatarImage = Transformations.distinctUntilChanged(Transformations.map(viewModel.getAvatar(), avatar -> Optional.fromNullable(avatar.getAvatar()))); LiveData<Optional<byte[]>> avatarImage = Transformations.map(LiveDataUtil.distinctUntilChanged(viewModel.getAvatar(), (b1, b2) -> Arrays.equals(b1.getAvatar(), b2.getAvatar())),
b -> Optional.fromNullable(b.getAvatar()));
avatarImage.observe(getViewLifecycleOwner(), this::presentAvatarImage); avatarImage.observe(getViewLifecycleOwner(), this::presentAvatarImage);
viewModel.getAvatar().observe(getViewLifecycleOwner(), this::presentAvatarPlaceholder); viewModel.getAvatar().observe(getViewLifecycleOwner(), this::presentAvatarPlaceholder);
@ -161,7 +165,7 @@ public class ManageProfileFragment extends LoggingFragment {
.circleCrop() .circleCrop()
.into(avatarView); .into(avatarView);
} else { } else {
avatarView.setImageDrawable(null); Glide.with(this).load((Drawable) null).into(avatarView);
} }
} }

Wyświetl plik

@ -57,7 +57,7 @@ final class ManageProfileRepository {
public void setAvatar(@NonNull Context context, @NonNull byte[] data, @NonNull String contentType, @NonNull Consumer<Result> callback) { public void setAvatar(@NonNull Context context, @NonNull byte[] data, @NonNull String contentType, @NonNull Consumer<Result> callback) {
SignalExecutors.UNBOUNDED.execute(() -> { SignalExecutors.UNBOUNDED.execute(() -> {
try { try {
ProfileUtil.uploadProfileWithAvatar(context, new StreamDetails(new ByteArrayInputStream(data), contentType, data.length)); ProfileUtil.uploadProfileWithAvatar(new StreamDetails(new ByteArrayInputStream(data), contentType, data.length));
AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data)); AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data));
SignalStore.misc().markHasEverHadAnAvatar(); SignalStore.misc().markHasEverHadAnAvatar();
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
@ -73,7 +73,7 @@ final class ManageProfileRepository {
public void clearAvatar(@NonNull Context context, @NonNull Consumer<Result> callback) { public void clearAvatar(@NonNull Context context, @NonNull Consumer<Result> callback) {
SignalExecutors.UNBOUNDED.execute(() -> { SignalExecutors.UNBOUNDED.execute(() -> {
try { try {
ProfileUtil.uploadProfileWithAvatar(context, null); ProfileUtil.uploadProfileWithAvatar(null);
AvatarHelper.delete(context, Recipient.self().getId()); AvatarHelper.delete(context, Recipient.self().getId());
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob()); ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());

Wyświetl plik

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
@ -30,6 +31,7 @@ import org.whispersystems.signalservice.api.util.StreamDetails;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
class ManageProfileViewModel extends ViewModel { class ManageProfileViewModel extends ViewModel {
@ -63,19 +65,6 @@ class ManageProfileViewModel extends ViewModel {
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
onRecipientChanged(Recipient.self().fresh()); onRecipientChanged(Recipient.self().fresh());
StreamDetails details = AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication());
if (details != null) {
try {
internalAvatarState.postValue(InternalAvatarState.loaded(StreamUtil.readFully(details.getStream())));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar!");
internalAvatarState.postValue(InternalAvatarState.none());
}
} else {
internalAvatarState.postValue(InternalAvatarState.none());
}
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId())); ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(Recipient.self().getId()));
}); });
@ -83,7 +72,7 @@ class ManageProfileViewModel extends ViewModel {
} }
public @NonNull LiveData<AvatarState> getAvatar() { public @NonNull LiveData<AvatarState> getAvatar() {
return avatarState; return Transformations.distinctUntilChanged(avatarState);
} }
public @NonNull LiveData<ProfileName> getProfileName() { public @NonNull LiveData<ProfileName> getProfileName() {
@ -169,6 +158,20 @@ class ManageProfileViewModel extends ViewModel {
about.postValue(recipient.getAbout()); about.postValue(recipient.getAbout());
aboutEmoji.postValue(recipient.getAboutEmoji()); aboutEmoji.postValue(recipient.getAboutEmoji());
badge.postValue(Optional.fromNullable(recipient.getFeaturedBadge())); badge.postValue(Optional.fromNullable(recipient.getFeaturedBadge()));
renderAvatar(AvatarHelper.getSelfProfileAvatarStream(ApplicationDependencies.getApplication()));
}
private void renderAvatar(@Nullable StreamDetails details) {
if (details != null) {
try {
internalAvatarState.postValue(InternalAvatarState.loaded(StreamUtil.readFully(details.getStream())));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar!");
internalAvatarState.postValue(InternalAvatarState.none());
}
} else {
internalAvatarState.postValue(InternalAvatarState.none());
}
} }
@Override @Override
@ -198,6 +201,19 @@ class ManageProfileViewModel extends ViewModel {
public @NonNull Recipient getSelf() { public @NonNull Recipient getSelf() {
return self; return self;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final AvatarState that = (AvatarState) o;
return Objects.equals(internalAvatarState, that.internalAvatarState) && Objects.equals(self, that.self);
}
@Override
public int hashCode() {
return Objects.hash(internalAvatarState, self);
}
} }
private final static class InternalAvatarState { private final static class InternalAvatarState {
@ -228,6 +244,21 @@ class ManageProfileViewModel extends ViewModel {
public LoadingState getLoadingState() { public LoadingState getLoadingState() {
return loadingState; return loadingState;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final InternalAvatarState that = (InternalAvatarState) o;
return Arrays.equals(avatar, that.avatar) && loadingState == that.loadingState;
}
@Override
public int hashCode() {
int result = Objects.hash(loadingState);
result = 31 * result + Arrays.hashCode(avatar);
return result;
}
} }
public enum LoadingState { public enum LoadingState {

Wyświetl plik

@ -227,8 +227,7 @@ public final class ProfileUtil {
public static void uploadProfileWithBadges(@NonNull Context context, @NonNull List<Badge> badges) throws IOException { public static void uploadProfileWithBadges(@NonNull Context context, @NonNull List<Badge> badges) throws IOException {
Log.d(TAG, "uploadProfileWithBadges()"); Log.d(TAG, "uploadProfileWithBadges()");
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
uploadProfile(context, uploadProfile(Recipient.self().getProfileName(),
Recipient.self().getProfileName(),
Optional.fromNullable(Recipient.self().getAbout()).or(""), Optional.fromNullable(Recipient.self().getAbout()).or(""),
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
getSelfPaymentsAddressProtobuf(), getSelfPaymentsAddressProtobuf(),
@ -245,8 +244,7 @@ public final class ProfileUtil {
public static void uploadProfileWithName(@NonNull Context context, @NonNull ProfileName profileName) throws IOException { public static void uploadProfileWithName(@NonNull Context context, @NonNull ProfileName profileName) throws IOException {
Log.d(TAG, "uploadProfileWithName()"); Log.d(TAG, "uploadProfileWithName()");
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
uploadProfile(context, uploadProfile(profileName,
profileName,
Optional.fromNullable(Recipient.self().getAbout()).or(""), Optional.fromNullable(Recipient.self().getAbout()).or(""),
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
getSelfPaymentsAddressProtobuf(), getSelfPaymentsAddressProtobuf(),
@ -263,8 +261,7 @@ public final class ProfileUtil {
public static void uploadProfileWithAbout(@NonNull Context context, @NonNull String about, @NonNull String emoji) throws IOException { public static void uploadProfileWithAbout(@NonNull Context context, @NonNull String about, @NonNull String emoji) throws IOException {
Log.d(TAG, "uploadProfileWithAbout()"); Log.d(TAG, "uploadProfileWithAbout()");
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
uploadProfile(context, uploadProfile(Recipient.self().getProfileName(),
Recipient.self().getProfileName(),
about, about,
emoji, emoji,
getSelfPaymentsAddressProtobuf(), getSelfPaymentsAddressProtobuf(),
@ -279,7 +276,7 @@ public final class ProfileUtil {
public static void uploadProfile(@NonNull Context context) throws IOException { public static void uploadProfile(@NonNull Context context) throws IOException {
Log.d(TAG, "uploadProfile()"); Log.d(TAG, "uploadProfile()");
try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) {
uploadProfileWithAvatar(context, avatar); uploadProfileWithAvatar(avatar);
} }
} }
@ -288,10 +285,9 @@ public final class ProfileUtil {
* avatar instead. This is useful when you want to ensure that the profile has been uploaded * avatar instead. This is useful when you want to ensure that the profile has been uploaded
* successfully before persisting the change to disk. * successfully before persisting the change to disk.
*/ */
public static void uploadProfileWithAvatar(@NonNull Context context, @Nullable StreamDetails avatar) throws IOException { public static void uploadProfileWithAvatar(@Nullable StreamDetails avatar) throws IOException {
Log.d(TAG, "uploadProfileWithAvatar()"); Log.d(TAG, "uploadProfileWithAvatar()");
uploadProfile(context, uploadProfile(Recipient.self().getProfileName(),
Recipient.self().getProfileName(),
Optional.fromNullable(Recipient.self().getAbout()).or(""), Optional.fromNullable(Recipient.self().getAbout()).or(""),
Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""),
getSelfPaymentsAddressProtobuf(), getSelfPaymentsAddressProtobuf(),
@ -299,8 +295,7 @@ public final class ProfileUtil {
Recipient.self().getBadges()); Recipient.self().getBadges());
} }
private static void uploadProfile(@NonNull Context context, private static void uploadProfile(@NonNull ProfileName profileName,
@NonNull ProfileName profileName,
@Nullable String about, @Nullable String about,
@Nullable String aboutEmoji, @Nullable String aboutEmoji,
@Nullable SignalServiceProtos.PaymentAddress paymentsAddress, @Nullable SignalServiceProtos.PaymentAddress paymentsAddress,
@ -308,7 +303,6 @@ public final class ProfileUtil {
@NonNull List<Badge> badges) @NonNull List<Badge> badges)
throws IOException throws IOException
{ {
List<String> badgeIds = badges.stream() List<String> badgeIds = badges.stream()
.filter(Badge::getVisible) .filter(Badge::getVisible)
.map(Badge::getId) .map(Badge::getId)
@ -333,6 +327,7 @@ public final class ProfileUtil {
badgeIds).orNull(); badgeIds).orNull();
SignalStore.registrationValues().markHasUploadedProfile(); SignalStore.registrationValues().markHasUploadedProfile();
SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath); SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath);
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
} }
private static @Nullable SignalServiceProtos.PaymentAddress getSelfPaymentsAddressProtobuf() { private static @Nullable SignalServiceProtos.PaymentAddress getSelfPaymentsAddressProtobuf() {