flash background after scrolling to message

This commit is contained in:
Daniel Gultsch 2023-03-31 14:14:20 +02:00
parent 4f654044b4
commit acfcde8416
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
7 changed files with 173 additions and 43 deletions

View file

@ -115,12 +115,23 @@ public final class MessageWithContentReactions
return Iterables.tryFind(this.contents, c -> c.type == PartType.FILE).isPresent(); 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() { public boolean hasInReplyTo() {
return this.inReplyTo != null; return this.inReplyTo != null;
} }
public Instant inReplyToSentAt() { public EmbeddedSentAt inReplyToSentAt() {
return this.inReplyTo == null ? null : this.inReplyTo.sentAt; if (this.inReplyTo == null) {
return null;
}
return new EmbeddedSentAt(this.sentAt, this.inReplyTo.sentAt);
} }
public String inReplyToSender() { public String inReplyToSender() {
@ -152,9 +163,6 @@ public final class MessageWithContentReactions
public AvatarWithAccount getAvatar() { public AvatarWithAccount getAvatar() {
final var address = getAddressWithName(); final var address = getAddressWithName();
if (address == null) {
return null;
}
if (isKnownSender()) { if (isKnownSender()) {
if (this.senderAvatar != null) { if (this.senderAvatar != null) {
return new AvatarWithAccount(accountId, address, AvatarType.PEP, this.senderAvatar); return new AvatarWithAccount(accountId, address, AvatarType.PEP, this.senderAvatar);
@ -318,4 +326,14 @@ public final class MessageWithContentReactions
READ, READ,
ERROR 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;
}
}
} }

View file

@ -64,30 +64,45 @@ public class BindingAdapters {
if (instant == null || instant.getEpochSecond() <= 0) { if (instant == null || instant.getEpochSecond() <= 0) {
textView.setVisibility(View.GONE); textView.setVisibility(View.GONE);
} else { } else {
final Context context = textView.getContext(); setDatetime(textView, Instant.now(), instant);
final Instant now = Instant.now(); }
textView.setVisibility(View.VISIBLE); }
if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) {
textView.setText( @BindingAdapter("datetime")
DateUtils.formatDateTime( public static void setDatetime(
context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME)); final TextView textView,
} else if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) { final MessageWithContentReactions.EmbeddedSentAt embeddedSentAt) {
textView.setText( if (embeddedSentAt == null || embeddedSentAt.embeddedSentAt.getEpochSecond() <= 0) {
DateUtils.formatDateTime( textView.setVisibility(View.GONE);
context, } else {
instant.toEpochMilli(), setDatetime(textView, embeddedSentAt.sentAt, embeddedSentAt.embeddedSentAt);
DateUtils.FORMAT_SHOW_DATE }
| DateUtils.FORMAT_NO_YEAR }
| DateUtils.FORMAT_ABBREV_ALL));
} else { private static void setDatetime(
textView.setText( final TextView textView, final Instant now, final Instant instant) {
DateUtils.formatDateTime( final Context context = textView.getContext();
context, textView.setVisibility(View.VISIBLE);
instant.toEpochMilli(), if (sameDay(instant, now) || now.minus(SIX_HOURS).isBefore(instant)) {
DateUtils.FORMAT_SHOW_DATE textView.setText(
| DateUtils.FORMAT_NO_MONTH_DAY DateUtils.formatDateTime(
| DateUtils.FORMAT_ABBREV_ALL)); 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));
} }
} }

View file

@ -15,6 +15,7 @@ import im.conversations.android.databinding.ItemMessageReceivedBinding;
import im.conversations.android.databinding.ItemMessageSentBinding; import im.conversations.android.databinding.ItemMessageSentBinding;
import im.conversations.android.databinding.ItemMessageSeparatorBinding; import im.conversations.android.databinding.ItemMessageSeparatorBinding;
import im.conversations.android.ui.AvatarFetcher; import im.conversations.android.ui.AvatarFetcher;
import im.conversations.android.ui.graphics.drawable.FlashBackgroundDrawable;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -93,6 +94,11 @@ public class MessageAdapter
@NonNull AbstractMessageViewHolder holder, @NonNull AbstractMessageViewHolder holder,
@NonNull final MessageWithContentReactions message) { @NonNull final MessageWithContentReactions message) {
holder.setItem(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; final var inReplyTo = message.inReplyTo;
if (holder instanceof MessageReceivedViewHolder messageReceivedViewHolder) { if (holder instanceof MessageReceivedViewHolder messageReceivedViewHolder) {
if (inReplyTo != null) { if (inReplyTo != null) {

View file

@ -21,6 +21,7 @@ import im.conversations.android.ui.RecyclerViewScroller;
import im.conversations.android.ui.adapter.MessageAdapter; import im.conversations.android.ui.adapter.MessageAdapter;
import im.conversations.android.ui.adapter.MessageAdapterItems; import im.conversations.android.ui.adapter.MessageAdapterItems;
import im.conversations.android.ui.adapter.MessageComparator; 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.ui.model.ChatViewModel;
import im.conversations.android.util.MainThreadExecutor; import im.conversations.android.util.MainThreadExecutor;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -108,6 +109,7 @@ public class ChatFragment extends Fragment {
} }
private void scrollToMessageId(final long messageId) { private void scrollToMessageId(final long messageId) {
// TODO do not scroll if view is fully visible
LOGGER.info("scrollToMessageId({})", messageId); LOGGER.info("scrollToMessageId({})", messageId);
this.chatViewModel.setShowDateSeparators(false); this.chatViewModel.setShowDateSeparators(false);
final var future = this.chatViewModel.getMessagePosition(messageId); final var future = this.chatViewModel.getMessagePosition(messageId);
@ -117,7 +119,11 @@ public class ChatFragment extends Fragment {
@Override @Override
public void onSuccess(final @NonNull Integer position) { public void onSuccess(final @NonNull Integer position) {
recyclerViewScroller.scrollToPosition( recyclerViewScroller.scrollToPosition(
position, () -> chatViewModel.setShowDateSeparators(true)); position,
() -> {
chatViewModel.setShowDateSeparators(true);
flashBackgroundAtPosition(position, messageId);
});
} }
@Override @Override
@ -128,4 +134,15 @@ public class ChatFragment extends Fragment {
}, },
MainThreadExecutor.getInstance()); 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);
}
}
} }

View file

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

View file

@ -44,7 +44,8 @@
android:visibility="@{message.hasInReplyTo ? View.VISIBLE : View.GONE}" android:visibility="@{message.hasInReplyTo ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView <TextView
android:id="@+id/embeddedMessageSender" android:id="@+id/embeddedMessageSender"
@ -86,12 +87,29 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintBottom_toTopOf="@+id/textContentWrapper" android:visibility="@{message.hasPreview ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@+id/downloadButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="@dimen/message_preview_max_height" app:layout_constraintHeight_max="@dimen/message_preview_max_height"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/embeddedMessage" app:layout_constraintTop_toBottomOf="@+id/embeddedMessage"
app:layout_constraintWidth_max="@dimen/message_preview_max_width" /> app:layout_constraintWidth_max="@dimen/message_preview_max_width"
tools:visibility="gone" />
<Button
android:id="@+id/downloadButton"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/check_x_filesize_on_host"
android:visibility="@{message.hasDownloadButton ? View.VISIBLE : View.GONE}"
app:icon="@drawable/ic_download_24dp"
app:layout_constraintBottom_toTopOf="@+id/textContentWrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/textContentWrapper" android:id="@+id/textContentWrapper"
@ -99,11 +117,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:visibility="visible" android:visibility="@{message.hasTextContent ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"> app:layout_constraintTop_toBottomOf="@+id/downloadButton">
<TextView <TextView
android:id="@+id/textContent" android:id="@+id/textContent"
@ -114,8 +132,6 @@
android:textColor="?colorOnSecondaryContainer" android:textColor="?colorOnSecondaryContainer"
tools:text="Quisque sit amet metus faucibus, egestas est eu, hendrerit mauris. Suspendisse pretium nisl purus, vitae vestibulum sapien rhoncus nec. Quisque molestie ante felis, vel dapibus ex mattis a. Morbi venenatis vestibulum neque, vel ornare sapien. Aliquam erat volutpat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. " /> tools:text="Quisque sit amet metus faucibus, egestas est eu, hendrerit mauris. Suspendisse pretium nisl purus, vitae vestibulum sapien rhoncus nec. Quisque molestie ante felis, vel dapibus ex mattis a. Morbi venenatis vestibulum neque, vel ornare sapien. Aliquam erat volutpat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. " />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -16,7 +16,6 @@
android:background="@drawable/background_message_bubble" android:background="@drawable/background_message_bubble"
android:backgroundTint="?colorTertiaryContainer" android:backgroundTint="?colorTertiaryContainer"
android:minHeight="40dp" android:minHeight="40dp"
android:padding="8dp"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0" app:layout_constraintHorizontal_bias="1.0"
@ -34,7 +33,8 @@
android:visibility="@{message.hasInReplyTo ? View.VISIBLE : View.GONE}" android:visibility="@{message.hasInReplyTo ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView <TextView
android:id="@+id/embeddedMessageSender" android:id="@+id/embeddedMessageSender"
@ -76,12 +76,29 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:adjustViewBounds="true"
app:layout_constraintBottom_toTopOf="@+id/textContentWrapper" android:visibility="@{message.hasPreview ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toTopOf="@+id/downloadButton"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="@dimen/message_preview_max_height" app:layout_constraintHeight_max="@dimen/message_preview_max_height"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/embeddedMessage" app:layout_constraintTop_toBottomOf="@+id/embeddedMessage"
app:layout_constraintWidth_max="@dimen/message_preview_max_width" /> app:layout_constraintWidth_max="@dimen/message_preview_max_width"
tools:visibility="gone" />
<Button
android:id="@+id/downloadButton"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/check_x_filesize_on_host"
android:visibility="@{message.hasDownloadButton ? View.VISIBLE : View.GONE}"
app:icon="@drawable/ic_download_24dp"
app:layout_constraintBottom_toTopOf="@+id/textContentWrapper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"
tools:visibility="visible" />
<LinearLayout <LinearLayout
android:id="@+id/textContentWrapper" android:id="@+id/textContentWrapper"
@ -89,11 +106,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:visibility="visible" android:visibility="@{message.hasTextContent ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image"> app:layout_constraintTop_toBottomOf="@+id/downloadButton">
<TextView <TextView
android:id="@+id/textContent" android:id="@+id/textContent"