From 34ef8b52f6266786837f30a10e35f608097e83db Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Wed, 23 Sep 2020 14:47:00 -0300 Subject: [PATCH] Display a loading message if group update message is taking a while to load. --- .../securesms/BindableConversationItem.java | 4 +- .../conversation/ConversationAdapter.java | 9 +- .../conversation/ConversationFragment.java | 2 +- .../conversation/ConversationItem.java | 8 +- .../conversation/ConversationUpdateItem.java | 98 ++++++++++--------- .../MessageDetailsActivity.java | 2 +- .../messagedetails/MessageDetailsAdapter.java | 11 ++- .../MessageHeaderViewHolder.java | 10 +- .../securesms/util/livedata/LiveDataUtil.java | 39 +++++++- app/src/main/res/values/strings.xml | 3 + 10 files changed, 122 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 81a7f14db..b77ab1d1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -4,6 +4,7 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.conversation.ConversationMessage; @@ -22,7 +23,8 @@ import java.util.Locale; import java.util.Set; public interface BindableConversationItem extends Unbindable { - void bind(@NonNull ConversationMessage messageRecord, + void bind(@NonNull LifecycleOwner lifecycleOwner, + @NonNull ConversationMessage messageRecord, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, @NonNull GlideRequests glideRequests, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java index 6b57f5f12..bad8da4f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationAdapter.java @@ -27,6 +27,7 @@ import androidx.annotation.LayoutRes; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; import androidx.paging.PagedList; import androidx.paging.PagedListAdapter; import androidx.recyclerview.widget.DiffUtil; @@ -84,6 +85,7 @@ public class ConversationAdapter private static final long FOOTER_ID = Long.MIN_VALUE + 1; private final ItemClickListener clickListener; + private final LifecycleOwner lifecycleOwner; private final GlideRequests glideRequests; private final Locale locale; private final Recipient recipient; @@ -99,12 +101,14 @@ public class ConversationAdapter private View headerView; private View footerView; - ConversationAdapter(@NonNull GlideRequests glideRequests, + ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner, + @NonNull GlideRequests glideRequests, @NonNull Locale locale, @Nullable ItemClickListener clickListener, @NonNull Recipient recipient) { super(new DiffCallback()); + this.lifecycleOwner = lifecycleOwner; this.glideRequests = glideRequests; this.locale = locale; @@ -216,7 +220,8 @@ public class ConversationAdapter ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; - conversationViewHolder.getBindable().bind(conversationMessage, + conversationViewHolder.getBindable().bind(lifecycleOwner, + conversationMessage, Optional.fromNullable(previousMessage != null ? previousMessage.getMessageRecord() : null), Optional.fromNullable(nextMessage != null ? nextMessage.getMessageRecord() : null), glideRequests, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 690615fe7..6c710cdc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -479,7 +479,7 @@ public class ConversationFragment extends LoggingFragment { private void initializeListAdapter() { if (this.recipient != null && this.threadId != -1) { Log.d(TAG, "Initializing adapter for " + recipient.getId()); - ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get()); + ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get()); list.setAdapter(adapter); setStickyHeaderDecoration(adapter); ConversationAdapter.initializePool(list.getRecycledViewPool()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index b16d09f41..714334c83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -54,6 +54,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; import com.annimon.stream.Stream; @@ -79,8 +80,6 @@ import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -116,9 +115,9 @@ import org.thoughtcrime.securesms.util.LongClickMovementMethod; import org.thoughtcrime.securesms.util.SearchUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; +import org.thoughtcrime.securesms.util.UrlClickHandler; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.VibrateUtil; -import org.thoughtcrime.securesms.util.UrlClickHandler; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; @@ -250,7 +249,8 @@ public class ConversationItem extends LinearLayout implements BindableConversati } @Override - public void bind(@NonNull ConversationMessage conversationMessage, + public void bind(@NonNull LifecycleOwner lifecycleOwner, + @NonNull ConversationMessage conversationMessage, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, @NonNull GlideRequests glideRequests, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 8405bb7f1..76ae6f78b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -14,6 +14,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.Transformations; @@ -29,13 +30,12 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.Debouncer; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.whispersystems.libsignal.util.guava.Optional; import java.util.Locale; @@ -44,9 +44,7 @@ import java.util.Set; import java.util.concurrent.ExecutionException; public final class ConversationUpdateItem extends LinearLayout - implements RecipientForeverObserver, - BindableConversationItem, - Observer + implements BindableConversationItem { private static final String TAG = ConversationUpdateItem.class.getSimpleName(); @@ -62,7 +60,8 @@ public final class ConversationUpdateItem extends LinearLayout private Locale locale; private LiveData displayBody; - private final Debouncer bodyClearDebouncer = new Debouncer(150); + private final UpdateObserver updateObserver = new UpdateObserver(); + private final SenderObserver senderObserver = new SenderObserver(); public ConversationUpdateItem(Context context) { super(context); @@ -85,7 +84,8 @@ public final class ConversationUpdateItem extends LinearLayout } @Override - public void bind(@NonNull ConversationMessage conversationMessage, + public void bind(@NonNull LifecycleOwner lifecycleOwner, + @NonNull ConversationMessage conversationMessage, @NonNull Optional previousMessageRecord, @NonNull Optional nextMessageRecord, @NonNull GlideRequests glideRequests, @@ -97,13 +97,7 @@ public final class ConversationUpdateItem extends LinearLayout { this.batchSelected = batchSelected; - bind(conversationMessage, locale); - } - - @Override - protected void onDetachedFromWindow() { - unbind(); - super.onDetachedFromWindow(); + bind(lifecycleOwner, conversationMessage, locale); } @Override @@ -116,49 +110,66 @@ public final class ConversationUpdateItem extends LinearLayout return conversationMessage; } - private void bind(@NonNull ConversationMessage conversationMessage, @NonNull Locale locale) { - if (this.sender != null) { - this.sender.removeForeverObserver(this); - } - - observeDisplayBody(null); - setBodyText(null); - + private void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage, @NonNull Locale locale) { this.conversationMessage = conversationMessage; this.messageRecord = conversationMessage.getMessageRecord(); - this.sender = messageRecord.getIndividualRecipient().live(); this.locale = locale; - this.sender.observeForever(this); + observeSender(lifecycleOwner, messageRecord.getIndividualRecipient()); UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext())); LiveData liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(updateDescription); - LiveData spannableStringMessage = Transformations.map(liveUpdateMessage, SpannableString::new); + LiveData spannableStringMessage = toSpannable(loading(liveUpdateMessage)); present(conversationMessage); - observeDisplayBody(spannableStringMessage); + observeDisplayBody(lifecycleOwner, spannableStringMessage); } - private void observeDisplayBody(@Nullable LiveData displayBody) { + /** After a short delay, if the main data hasn't shown yet, then a loading message is displayed. */ + private @NonNull LiveData loading(@NonNull LiveData string) { + return LiveDataUtil.until(string, LiveDataUtil.delay(250, getContext().getString(R.string.ConversationUpdateItem_loading))); + } + + private static LiveData toSpannable(LiveData loading) { + return Transformations.map(loading, source -> source == null ? null : new SpannableString(source)); + } + + @Override + public void unbind() { + } + + private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) { + if (sender != null) { + sender.getLiveData().removeObserver(senderObserver); + } + + if (recipient != null) { + sender = recipient.live(); + sender.getLiveData().observe(lifecycleOwner, senderObserver); + } else { + sender = null; + } + } + + private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData displayBody) { if (this.displayBody != displayBody) { if (this.displayBody != null) { - this.displayBody.removeObserver(this); + this.displayBody.removeObserver(updateObserver); } this.displayBody = displayBody; if (this.displayBody != null) { - this.displayBody.observeForever(this); + this.displayBody.observe(lifecycleOwner, updateObserver); } } } private void setBodyText(@Nullable CharSequence text) { if (text == null) { - bodyClearDebouncer.publish(() -> body.setText(null)); + body.setVisibility(INVISIBLE); } else { - bodyClearDebouncer.clear(); body.setText(text); body.setVisibility(VISIBLE); } @@ -257,28 +268,25 @@ public final class ConversationUpdateItem extends LinearLayout icon.setColorFilter(getIconTintFilter()); } - @Override - public void onRecipientChanged(@NonNull Recipient recipient) { - present(conversationMessage); - } - @Override public void setOnClickListener(View.OnClickListener l) { super.setOnClickListener(new InternalClickListener(l)); } - @Override - public void unbind() { - if (sender != null) { - sender.removeForeverObserver(this); - } + private final class SenderObserver implements Observer { - observeDisplayBody(null); + @Override + public void onChanged(Recipient recipient) { + present(conversationMessage); + } } - @Override - public void onChanged(SpannableString update) { - setBodyText(update); + private final class UpdateObserver implements Observer { + + @Override + public void onChanged(SpannableString update) { + setBodyText(update); + } } private class InternalClickListener implements View.OnClickListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java index 193a3f413..c9003c974 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsActivity.java @@ -91,7 +91,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity { private void initializeList() { RecyclerView list = findViewById(R.id.message_details_list); - adapter = new MessageDetailsAdapter(glideRequests); + adapter = new MessageDetailsAdapter(this, glideRequests); list.setAdapter(adapter); list.setItemAnimator(null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java index 92fd33068..9984261f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsAdapter.java @@ -5,6 +5,7 @@ import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; @@ -20,13 +21,15 @@ final class MessageDetailsAdapter extends ListAdapter(), conversationMessage.getMessageRecord().getRecipient(), null, false); + conversationItem.bind(lifecycleOwner, conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false); } private void bindErrorState(MessageRecord messageRecord) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java index 8e949ea2f..6f78577d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.util.livedata; +import android.os.Handler; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; @@ -11,7 +13,6 @@ import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.whispersystems.libsignal.util.guava.Function; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -86,7 +87,7 @@ public final class LiveDataUtil { * Merges the supplied live data streams. */ public static LiveData merge(@NonNull List> liveDataList) { - Set> set = new LinkedHashSet<>(liveDataList); + Set> set = new LinkedHashSet<>(liveDataList.size()); set.addAll(liveDataList); @@ -110,6 +111,40 @@ public final class LiveDataUtil { return new MutableLiveData<>(item); } + /** + * Emits {@param whileWaiting} until {@param main} starts emitting. + */ + public static @NonNull LiveData until(@NonNull LiveData main, + @NonNull LiveData whileWaiting) + { + MediatorLiveData mediatorLiveData = new MediatorLiveData<>(); + + mediatorLiveData.addSource(whileWaiting, mediatorLiveData::setValue); + + mediatorLiveData.addSource(main, value -> { + mediatorLiveData.removeSource(whileWaiting); + mediatorLiveData.setValue(value); + }); + + return mediatorLiveData; + } + + /** + * After {@param delay} ms after observation, emits a single Object, {@param value}. + */ + public static LiveData delay(long delay, T value) { + return new MutableLiveData() { + boolean emittedValue; + + @Override + protected void onActive() { + if (emittedValue) return; + new Handler().postDelayed(() -> setValue(value), delay); + emittedValue = true; + } + }; + } + public interface Combine { @NonNull R apply(@NonNull A a, @NonNull B b); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36867031a..4811b26aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1701,6 +1701,9 @@ Contact photo + + Loading + Play … Pause Download