From acfcde84163db9aa1c1de37766e29e0ff01a198a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 31 Mar 2023 14:14:20 +0200 Subject: [PATCH] flash background after scrolling to message --- .../model/MessageWithContentReactions.java | 28 +++++++-- .../android/ui/BindingAdapters.java | 63 ++++++++++++------- .../android/ui/adapter/MessageAdapter.java | 6 ++ .../ui/fragment/main/ChatFragment.java | 19 +++++- .../drawable/FlashBackgroundDrawable.java | 41 ++++++++++++ .../main/res/layout/item_message_received.xml | 30 ++++++--- app/src/main/res/layout/item_message_sent.xml | 29 +++++++-- 7 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/im/conversations/android/ui/graphics/drawable/FlashBackgroundDrawable.java diff --git a/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java b/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java index 97f24ddb5..4ba8dbc68 100644 --- a/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java +++ b/app/src/main/java/im/conversations/android/database/model/MessageWithContentReactions.java @@ -115,12 +115,23 @@ public final class MessageWithContentReactions return Iterables.tryFind(this.contents, c -> c.type == PartType.FILE).isPresent(); } + public boolean hasDownloadButton() { + return hasPreview(); + } + + public boolean hasTextContent() { + return Iterables.tryFind(this.contents, c -> c.type == PartType.TEXT).isPresent(); + } + public boolean hasInReplyTo() { return this.inReplyTo != null; } - public Instant inReplyToSentAt() { - return this.inReplyTo == null ? null : this.inReplyTo.sentAt; + public EmbeddedSentAt inReplyToSentAt() { + if (this.inReplyTo == null) { + return null; + } + return new EmbeddedSentAt(this.sentAt, this.inReplyTo.sentAt); } public String inReplyToSender() { @@ -152,9 +163,6 @@ public final class MessageWithContentReactions public AvatarWithAccount getAvatar() { final var address = getAddressWithName(); - if (address == null) { - return null; - } if (isKnownSender()) { if (this.senderAvatar != null) { return new AvatarWithAccount(accountId, address, AvatarType.PEP, this.senderAvatar); @@ -318,4 +326,14 @@ public final class MessageWithContentReactions READ, ERROR } + + public static class EmbeddedSentAt { + public final Instant sentAt; + public final Instant embeddedSentAt; + + public EmbeddedSentAt(Instant sentAt, Instant embeddedSentAt) { + this.sentAt = sentAt; + this.embeddedSentAt = embeddedSentAt; + } + } } diff --git a/app/src/main/java/im/conversations/android/ui/BindingAdapters.java b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java index 4fca5f824..e529b6272 100644 --- a/app/src/main/java/im/conversations/android/ui/BindingAdapters.java +++ b/app/src/main/java/im/conversations/android/ui/BindingAdapters.java @@ -64,30 +64,45 @@ public class BindingAdapters { if (instant == null || instant.getEpochSecond() <= 0) { textView.setVisibility(View.GONE); } else { - final Context context = textView.getContext(); - final Instant now = Instant.now(); - textView.setVisibility(View.VISIBLE); - if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) { - textView.setText( - DateUtils.formatDateTime( - context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME)); - } else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) { - textView.setText( - DateUtils.formatDateTime( - context, - instant.toEpochMilli(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NO_YEAR - | DateUtils.FORMAT_ABBREV_ALL)); - } else { - textView.setText( - DateUtils.formatDateTime( - context, - instant.toEpochMilli(), - DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NO_MONTH_DAY - | DateUtils.FORMAT_ABBREV_ALL)); - } + setDatetime(textView, Instant.now(), instant); + } + } + + @BindingAdapter("datetime") + public static void setDatetime( + final TextView textView, + final MessageWithContentReactions.EmbeddedSentAt embeddedSentAt) { + if (embeddedSentAt == null || embeddedSentAt.embeddedSentAt.getEpochSecond() <= 0) { + textView.setVisibility(View.GONE); + } else { + setDatetime(textView, embeddedSentAt.sentAt, embeddedSentAt.embeddedSentAt); + } + } + + private static void setDatetime( + final TextView textView, final Instant now, final Instant instant) { + final Context context = textView.getContext(); + textView.setVisibility(View.VISIBLE); + if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) { + textView.setText( + DateUtils.formatDateTime( + context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME)); + } else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) { + textView.setText( + DateUtils.formatDateTime( + context, + instant.toEpochMilli(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NO_YEAR + | DateUtils.FORMAT_ABBREV_ALL)); + } else { + textView.setText( + DateUtils.formatDateTime( + context, + instant.toEpochMilli(), + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NO_MONTH_DAY + | DateUtils.FORMAT_ABBREV_ALL)); } } diff --git a/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java b/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java index dbfe3f920..8726b8142 100644 --- a/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java +++ b/app/src/main/java/im/conversations/android/ui/adapter/MessageAdapter.java @@ -15,6 +15,7 @@ import im.conversations.android.databinding.ItemMessageReceivedBinding; import im.conversations.android.databinding.ItemMessageSentBinding; import im.conversations.android.databinding.ItemMessageSeparatorBinding; import im.conversations.android.ui.AvatarFetcher; +import im.conversations.android.ui.graphics.drawable.FlashBackgroundDrawable; import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,6 +94,11 @@ public class MessageAdapter @NonNull AbstractMessageViewHolder holder, @NonNull final MessageWithContentReactions message) { holder.setItem(message); + if (holder.itemView.getBackground() instanceof FlashBackgroundDrawable backgroundDrawable) { + if (backgroundDrawable.needsReset(message.id)) { + holder.itemView.setBackground(null); + } + } final var inReplyTo = message.inReplyTo; if (holder instanceof MessageReceivedViewHolder messageReceivedViewHolder) { if (inReplyTo != null) { diff --git a/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java b/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java index 45f707040..2fa6f054b 100644 --- a/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java +++ b/app/src/main/java/im/conversations/android/ui/fragment/main/ChatFragment.java @@ -21,6 +21,7 @@ import im.conversations.android.ui.RecyclerViewScroller; import im.conversations.android.ui.adapter.MessageAdapter; import im.conversations.android.ui.adapter.MessageAdapterItems; import im.conversations.android.ui.adapter.MessageComparator; +import im.conversations.android.ui.graphics.drawable.FlashBackgroundDrawable; import im.conversations.android.ui.model.ChatViewModel; import im.conversations.android.util.MainThreadExecutor; import org.slf4j.Logger; @@ -108,6 +109,7 @@ public class ChatFragment extends Fragment { } private void scrollToMessageId(final long messageId) { + // TODO do not scroll if view is fully visible LOGGER.info("scrollToMessageId({})", messageId); this.chatViewModel.setShowDateSeparators(false); final var future = this.chatViewModel.getMessagePosition(messageId); @@ -117,7 +119,11 @@ public class ChatFragment extends Fragment { @Override public void onSuccess(final @NonNull Integer position) { recyclerViewScroller.scrollToPosition( - position, () -> chatViewModel.setShowDateSeparators(true)); + position, + () -> { + chatViewModel.setShowDateSeparators(true); + flashBackgroundAtPosition(position, messageId); + }); } @Override @@ -128,4 +134,15 @@ public class ChatFragment extends Fragment { }, MainThreadExecutor.getInstance()); } + + private void flashBackgroundAtPosition(final int position, final long messageId) { + final var layoutManager = this.binding.messages.getLayoutManager(); + if (layoutManager instanceof LinearLayoutManager llm) { + final var view = llm.findViewByPosition(position); + if (view == null) { + return; + } + FlashBackgroundDrawable.flashBackground(view, messageId); + } + } } diff --git a/app/src/main/java/im/conversations/android/ui/graphics/drawable/FlashBackgroundDrawable.java b/app/src/main/java/im/conversations/android/ui/graphics/drawable/FlashBackgroundDrawable.java new file mode 100644 index 000000000..01da6c962 --- /dev/null +++ b/app/src/main/java/im/conversations/android/ui/graphics/drawable/FlashBackgroundDrawable.java @@ -0,0 +1,41 @@ +package im.conversations.android.ui.graphics.drawable; + +import android.content.Context; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.ColorDrawable; +import android.view.View; +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import com.google.android.material.color.MaterialColors; + +public class FlashBackgroundDrawable extends AnimationDrawable { + + private final long messageId; + + private FlashBackgroundDrawable(final Context context, final long messageId) { + this.messageId = messageId; + @ColorInt + int backgroundColor = + MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorSurfaceVariant, + "colorSurfaceVariant not found"); + for (int i = 0; i < 3; ++i) { + this.addFrame(new ColorDrawable(backgroundColor), 250); + this.addFrame(new ColorDrawable(android.graphics.Color.TRANSPARENT), 250); + } + this.setEnterFadeDuration(125); + this.setExitFadeDuration(125); + this.setOneShot(true); + } + + public boolean needsReset(final long messageId) { + return this.messageId != messageId || !this.isRunning(); + } + + public static void flashBackground(@NonNull final View view, final long messageId) { + final var animationDrawable = new FlashBackgroundDrawable(view.getContext(), messageId); + view.setBackground(animationDrawable); + view.post(animationDrawable::start); + } +} diff --git a/app/src/main/res/layout/item_message_received.xml b/app/src/main/res/layout/item_message_received.xml index f4e1c7ce1..50cf7f856 100644 --- a/app/src/main/res/layout/item_message_received.xml +++ b/app/src/main/res/layout/item_message_received.xml @@ -44,7 +44,8 @@ android:visibility="@{message.hasInReplyTo ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> + app:layout_constraintWidth_max="@dimen/message_preview_max_width" + tools:visibility="gone" /> + +