add date separators
This commit is contained in:
parent
5b777ef657
commit
4bfcf209d7
|
@ -0,0 +1,15 @@
|
|||
package im.conversations.android.database.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public sealed interface MessageAdapterItem
|
||||
permits MessageAdapterItem.MessageDateSeparator, MessageWithContentReactions {
|
||||
|
||||
final class MessageDateSeparator implements MessageAdapterItem {
|
||||
public final Instant date;
|
||||
|
||||
public MessageDateSeparator(Instant date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,8 @@ import org.jxmpp.jid.impl.JidCreate;
|
|||
import org.jxmpp.jid.parts.Resourcepart;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
public class MessageWithContentReactions implements IndividualName, KnownSender {
|
||||
public final class MessageWithContentReactions
|
||||
implements IndividualName, KnownSender, MessageAdapterItem {
|
||||
|
||||
public long accountId;
|
||||
|
||||
|
|
|
@ -105,6 +105,29 @@ public class BindingAdapters {
|
|||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("date")
|
||||
public static void setDate(final TextView textView, final Instant instant) {
|
||||
if (instant == null || instant.toEpochMilli() <= 0) {
|
||||
textView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
final Context context = textView.getContext();
|
||||
final Instant now = Instant.now();
|
||||
if (sameYear(instant, now) || now.minus(THREE_MONTH).isBefore(instant)) {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
textView.setText(
|
||||
DateUtils.formatDateTime(
|
||||
context,
|
||||
instant.toEpochMilli(),
|
||||
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR));
|
||||
} else {
|
||||
textView.setVisibility(View.VISIBLE);
|
||||
textView.setText(
|
||||
DateUtils.formatDateTime(
|
||||
context, instant.toEpochMilli(), DateUtils.FORMAT_SHOW_DATE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
public static void setSender(final TextView textView, final ChatOverviewItem.Sender sender) {
|
||||
if (sender == null) {
|
||||
|
|
|
@ -9,32 +9,35 @@ import androidx.paging.PagingDataAdapter;
|
|||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.database.model.MessageAdapterItem;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MessageAdapter
|
||||
extends PagingDataAdapter<
|
||||
MessageWithContentReactions, MessageAdapter.AbstractMessageViewHolder> {
|
||||
extends PagingDataAdapter<MessageAdapterItem, MessageAdapter.AbstractMessageViewHolder> {
|
||||
|
||||
private static final int VIEW_TYPE_RECEIVED = 0;
|
||||
private static final int VIEW_TYPE_SENT = 1;
|
||||
private static final int VIEW_TYPE_SEPARATOR = 2;
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MessageAdapter.class);
|
||||
|
||||
public MessageAdapter(
|
||||
@NonNull DiffUtil.ItemCallback<MessageWithContentReactions> diffCallback) {
|
||||
public MessageAdapter(@NonNull DiffUtil.ItemCallback<MessageAdapterItem> diffCallback) {
|
||||
super(diffCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(final int position) {
|
||||
final var message = getItem(position);
|
||||
if (message != null && message.outgoing) {
|
||||
return VIEW_TYPE_SENT;
|
||||
final var item = peek(position);
|
||||
if (item instanceof MessageAdapterItem.MessageDateSeparator) {
|
||||
return VIEW_TYPE_SEPARATOR;
|
||||
} else if (item instanceof MessageWithContentReactions m) {
|
||||
return m.outgoing ? VIEW_TYPE_SENT : VIEW_TYPE_RECEIVED;
|
||||
} else {
|
||||
return VIEW_TYPE_RECEIVED;
|
||||
}
|
||||
|
@ -53,26 +56,47 @@ public class MessageAdapter
|
|||
return new MessageSentViewHolder(
|
||||
DataBindingUtil.inflate(
|
||||
layoutInflater, R.layout.item_message_sent, parent, false));
|
||||
} else if (viewType == VIEW_TYPE_SEPARATOR) {
|
||||
return new MessageDateSeparator(
|
||||
DataBindingUtil.inflate(
|
||||
layoutInflater, R.layout.item_message_separator, parent, false));
|
||||
}
|
||||
throw new IllegalArgumentException(String.format("viewType %d not implemented", viewType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AbstractMessageViewHolder holder, int position) {
|
||||
final var message = getItem(position);
|
||||
if (message == null) {
|
||||
holder.setMessage(null);
|
||||
final var item = getItem(position);
|
||||
if (item == null) {
|
||||
holder.setItem(null);
|
||||
} else if (item instanceof MessageWithContentReactions message) {
|
||||
this.onBindViewHolder(holder, message);
|
||||
} else if (item instanceof MessageAdapterItem.MessageDateSeparator dateSeparator) {
|
||||
this.onBindViewHolder(holder, dateSeparator);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"%s is not a known implementation", item.getClass().getSimpleName()));
|
||||
}
|
||||
LOGGER.info("onBindViewHolder({})", message == null ? null : message.id);
|
||||
holder.setMessage(message);
|
||||
}
|
||||
|
||||
private void onBindViewHolder(
|
||||
@NonNull final AbstractMessageViewHolder holder,
|
||||
@NonNull final MessageAdapterItem.MessageDateSeparator dateSeparator) {
|
||||
holder.setItem(dateSeparator);
|
||||
}
|
||||
|
||||
private void onBindViewHolder(
|
||||
@NonNull AbstractMessageViewHolder holder,
|
||||
@NonNull final MessageWithContentReactions message) {
|
||||
holder.setItem(message);
|
||||
if (holder instanceof MessageReceivedViewHolder messageReceivedViewHolder) {
|
||||
final var addressWithName = message == null ? null : message.getAddressWithName();
|
||||
final var avatar = message == null ? null : message.getAvatar();
|
||||
final var addressWithName = message.getAddressWithName();
|
||||
final var avatar = message.getAvatar();
|
||||
messageReceivedViewHolder.binding.avatar.setVisibility(View.VISIBLE);
|
||||
if (avatar != null) {
|
||||
messageReceivedViewHolder.binding.avatar.setVisibility(View.VISIBLE);
|
||||
AvatarFetcher.fetchInto(messageReceivedViewHolder.binding.avatar, avatar);
|
||||
} else if (addressWithName != null) {
|
||||
messageReceivedViewHolder.binding.avatar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
AvatarFetcher.setDefault(messageReceivedViewHolder.binding.avatar, addressWithName);
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +108,7 @@ public class MessageAdapter
|
|||
super(itemView);
|
||||
}
|
||||
|
||||
protected abstract void setMessage(final MessageWithContentReactions message);
|
||||
protected abstract void setItem(final MessageAdapterItem item);
|
||||
}
|
||||
|
||||
public static class MessageReceivedViewHolder extends AbstractMessageViewHolder {
|
||||
|
@ -97,8 +121,12 @@ public class MessageAdapter
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setMessage(final MessageWithContentReactions message) {
|
||||
protected void setItem(final MessageAdapterItem item) {
|
||||
if (item instanceof MessageWithContentReactions message) {
|
||||
this.binding.setMessage(message);
|
||||
} else {
|
||||
this.binding.setMessage(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,8 +140,31 @@ public class MessageAdapter
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setMessage(MessageWithContentReactions message) {
|
||||
protected void setItem(MessageAdapterItem item) {
|
||||
if (item instanceof MessageWithContentReactions message) {
|
||||
this.binding.setMessage(message);
|
||||
} else {
|
||||
this.binding.setMessage(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MessageDateSeparator extends AbstractMessageViewHolder {
|
||||
|
||||
private final ItemMessageSeparatorBinding binding;
|
||||
|
||||
private MessageDateSeparator(@NonNull ItemMessageSeparatorBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setItem(MessageAdapterItem item) {
|
||||
if (item instanceof MessageAdapterItem.MessageDateSeparator dateSeparator) {
|
||||
this.binding.setTimestamp(dateSeparator.date);
|
||||
} else {
|
||||
this.binding.setTimestamp(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,29 +2,44 @@ package im.conversations.android.ui.adapter;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import im.conversations.android.database.model.MessageAdapterItem;
|
||||
import im.conversations.android.database.model.MessageWithContentReactions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MessageComparator extends DiffUtil.ItemCallback<MessageWithContentReactions> {
|
||||
public class MessageComparator extends DiffUtil.ItemCallback<MessageAdapterItem> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MessageComparator.class);
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(
|
||||
@NonNull MessageWithContentReactions oldItem,
|
||||
@NonNull MessageWithContentReactions newItem) {
|
||||
return oldItem.id == newItem.id;
|
||||
@NonNull MessageAdapterItem oldItem, @NonNull MessageAdapterItem newItem) {
|
||||
if (oldItem instanceof MessageWithContentReactions oldMessage
|
||||
&& newItem instanceof MessageWithContentReactions newMessage) {
|
||||
return oldMessage.id == newMessage.id;
|
||||
} else if (oldItem instanceof MessageAdapterItem.MessageDateSeparator oldSeparator
|
||||
&& newItem instanceof MessageAdapterItem.MessageDateSeparator newSeparator) {
|
||||
return oldSeparator.date.equals(newSeparator.date);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(
|
||||
@NonNull MessageWithContentReactions oldItem,
|
||||
@NonNull MessageWithContentReactions newItem) {
|
||||
final var areContentsTheSame = oldItem.equals(newItem);
|
||||
@NonNull MessageAdapterItem oldItem, @NonNull MessageAdapterItem newItem) {
|
||||
if (oldItem instanceof MessageWithContentReactions oldMessage
|
||||
&& newItem instanceof MessageWithContentReactions newMessage) {
|
||||
final var areContentsTheSame = oldMessage.equals(newMessage);
|
||||
if (!areContentsTheSame) {
|
||||
LOGGER.info("Message {} got modified", oldItem.id);
|
||||
LOGGER.info("Message {} got modified", oldMessage.id);
|
||||
}
|
||||
return areContentsTheSame;
|
||||
} else if (oldItem instanceof MessageAdapterItem.MessageDateSeparator oldSeparator
|
||||
&& newItem instanceof MessageAdapterItem.MessageDateSeparator newSeparator) {
|
||||
return oldSeparator.date.equals(newSeparator.date);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,12 @@ import androidx.annotation.NonNull;
|
|||
import androidx.databinding.DataBindingUtil;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.paging.PagingData;
|
||||
import androidx.paging.PagingDataTransforms;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import im.conversations.android.R;
|
||||
import im.conversations.android.database.model.MessageAdapterItem;
|
||||
import im.conversations.android.databinding.FragmentChatBinding;
|
||||
import im.conversations.android.ui.Activities;
|
||||
import im.conversations.android.ui.NavControllers;
|
||||
|
@ -17,6 +21,7 @@ import im.conversations.android.ui.RecyclerViewScroller;
|
|||
import im.conversations.android.ui.adapter.MessageAdapter;
|
||||
import im.conversations.android.ui.adapter.MessageComparator;
|
||||
import im.conversations.android.ui.model.ChatViewModel;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -53,7 +58,35 @@ public class ChatFragment extends Fragment {
|
|||
.getMessages()
|
||||
.observe(
|
||||
getViewLifecycleOwner(),
|
||||
pagingData -> messageAdapter.submitData(getLifecycle(), pagingData));
|
||||
pagingData -> {
|
||||
final PagingData<MessageAdapterItem> foo =
|
||||
PagingDataTransforms.insertSeparators(
|
||||
pagingData,
|
||||
MoreExecutors.directExecutor(),
|
||||
(before, after) -> {
|
||||
final var dayBefore =
|
||||
before == null
|
||||
? null
|
||||
: before.sentAt.truncatedTo(
|
||||
ChronoUnit.DAYS);
|
||||
final var dayAfter =
|
||||
after == null
|
||||
? null
|
||||
: after.sentAt.truncatedTo(
|
||||
ChronoUnit.DAYS);
|
||||
if (dayAfter == null && dayBefore != null) {
|
||||
return new MessageAdapterItem
|
||||
.MessageDateSeparator(dayBefore);
|
||||
} else if (dayBefore == null
|
||||
|| dayBefore.equals(dayAfter)) {
|
||||
return null;
|
||||
} else {
|
||||
return new MessageAdapterItem
|
||||
.MessageDateSeparator(dayBefore);
|
||||
}
|
||||
});
|
||||
messageAdapter.submitData(getLifecycle(), foo);
|
||||
});
|
||||
this.binding.materialToolbar.setNavigationOnClickListener(
|
||||
view -> {
|
||||
NavControllers.findNavController(requireActivity(), R.id.nav_host_fragment)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?colorPrimaryContainer"/>
|
||||
<corners android:radius="8dp"/>
|
||||
</shape>
|
48
app/src/main/res/layout/item_message_separator.xml
Normal file
48
app/src/main/res/layout/item_message_separator.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="6dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:background="@drawable/background_message_separator"
|
||||
android:paddingVertical="2sp"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:date="@{timestamp}"
|
||||
android:textAppearance="?textAppearanceBodyMedium"
|
||||
android:textColor="?colorOnPrimaryContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="June 12th 2022" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="timestamp"
|
||||
type="java.time.Instant" />
|
||||
</data>
|
||||
</layout>
|
Loading…
Reference in a new issue