diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index cfe1cc60fafda27271c073c70fb88ac437bc2a1f..219c463f4bfac7f6aba98f28e4e65d837e9fa90f 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -2,11 +2,9 @@ package eu.siacs.conversations; import android.graphics.Bitmap; import android.net.Uri; - import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; - import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -81,7 +79,6 @@ public final class Config { public static final int CONNECT_DISCO_TIMEOUT = 20; public static final int MINI_GRACE_PERIOD = 750; - // media file formats. Homogenous Android or Conversations only deployments can switch to opus // and webp public static final int AVATAR_SIZE = 192; @@ -94,7 +91,7 @@ public final class Config { public static final boolean USE_OPUS_VOICE_MESSAGES = false; - public static final int MESSAGE_MERGE_WINDOW = 20; + public static final int MESSAGE_MERGE_WINDOW = 90_000; public static final int PAGE_SIZE = 50; public static final int MAX_NUM_PAGES = 3; diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index b29c1fac5bc05b60472c94768264d0a77fcc075e..ee61056fa96d1bc1900cad6b2bd1cbb83261c33e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -615,8 +615,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return this.remoteMsgId == null && matchingCounterpart && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) - < Config.MESSAGE_MERGE_WINDOW * 1000; + && Math.abs(this.getTimeSent() - message.getTimeSent()) < 20_000; } } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index e26977c5c291e7f95c1ca0906ba8fcbc6be0c4fe..1ad73d920f96a93a0de7fa37edb120fed16a584f 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -29,6 +29,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.widget.ImageViewCompat; @@ -152,7 +153,7 @@ public class MessageAdapter extends ArrayAdapter { return 5; } - private int getItemViewType(Message message) { + private static int getItemViewType(final Message message) { if (message.getType() == Message.TYPE_STATUS) { if (DATE_SEPARATOR_BODY.equals(message.getBody())) { return DATE_SEPARATOR; @@ -169,8 +170,8 @@ public class MessageAdapter extends ArrayAdapter { } @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); + public int getItemViewType(final int position) { + return getItemViewType(getItem(position)); } private void displayStatus( @@ -729,7 +730,7 @@ public class MessageAdapter extends ArrayAdapter { message.isValidInSession() && (!omemoEncryption || message.isTrusted()); final Conversational conversation = message.getConversation(); final Account account = conversation.getAccount(); - final int type = getItemViewType(position); + final int type = getItemViewType(message); ViewHolder viewHolder; if (view == null) { viewHolder = new ViewHolder(); @@ -753,6 +754,7 @@ public class MessageAdapter extends ArrayAdapter { view = activity.getLayoutInflater() .inflate(R.layout.item_message_sent, parent, false); + viewHolder.root = (ConstraintLayout) view; viewHolder.message_box = view.findViewById(R.id.message_box); viewHolder.contact_picture = view.findViewById(R.id.message_photo); viewHolder.download_button = view.findViewById(R.id.download_button); @@ -769,6 +771,7 @@ public class MessageAdapter extends ArrayAdapter { view = activity.getLayoutInflater() .inflate(R.layout.item_message_received, parent, false); + viewHolder.root = (ConstraintLayout) view; viewHolder.message_box = view.findViewById(R.id.message_box); viewHolder.contact_picture = view.findViewById(R.id.message_photo); viewHolder.download_button = view.findViewById(R.id.download_button); @@ -901,7 +904,7 @@ public class MessageAdapter extends ArrayAdapter { } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null - && message.getCounterparts().size() > 0)) { + && !message.getCounterparts().isEmpty())) { showAvatar = true; AvatarWorkerTask.loadAvatar( message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); @@ -917,6 +920,12 @@ public class MessageAdapter extends ArrayAdapter { } return view; } else { + // sent and received bubbles + final var mergeIntoTop = mergeIntoTop(position, message); + final var mergeIntoBottom = mergeIntoBottom(position, message); + final var requiresAvatar = type == SENT ? !mergeIntoBottom : !mergeIntoTop; + setBubblePadding(viewHolder.root, mergeIntoTop, mergeIntoBottom); + setRequiresAvatar(viewHolder, requiresAvatar); viewHolder.message_box.setClipToOutline(true); AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); } @@ -1075,6 +1084,81 @@ public class MessageAdapter extends ArrayAdapter { return view; } + private void setBubblePadding( + final ConstraintLayout root, + final boolean mergeIntoTop, + final boolean mergeIntoBottom) { + final var resources = root.getResources(); + final var horizontal = resources.getDimensionPixelSize(R.dimen.bubble_horizontal_padding); + final int top = + resources.getDimensionPixelSize( + mergeIntoTop + ? R.dimen.bubble_vertical_padding_minimum + : R.dimen.bubble_vertical_padding); + final int bottom = + resources.getDimensionPixelSize( + mergeIntoBottom + ? R.dimen.bubble_vertical_padding_minimum + : R.dimen.bubble_vertical_padding); + root.setPadding(horizontal, top, horizontal, bottom); + } + + private void setRequiresAvatar(final ViewHolder viewHolder, final boolean requiresAvatar) { + final var layoutParams = viewHolder.contact_picture.getLayoutParams(); + if (requiresAvatar) { + final var resources = viewHolder.contact_picture.getResources(); + final var avatarSize = resources.getDimensionPixelSize(R.dimen.bubble_avatar_size); + layoutParams.height = avatarSize; + viewHolder.contact_picture.setVisibility(View.VISIBLE); + viewHolder.message_box.setMinimumHeight(avatarSize); + } else { + layoutParams.height = 0; + viewHolder.contact_picture.setVisibility(View.INVISIBLE); + viewHolder.message_box.setMinimumHeight(0); + } + viewHolder.contact_picture.setLayoutParams(layoutParams); + } + + private boolean mergeIntoTop(final int position, final Message message) { + if (position < 0) { + return false; + } + final var top = getItem(position - 1); + return merge(top, message); + } + + private boolean mergeIntoBottom(final int position, final Message message) { + final Message bottom; + try { + bottom = getItem(position + 1); + } catch (final IndexOutOfBoundsException e) { + return false; + } + return merge(message, bottom); + } + + private static boolean merge(final Message a, final Message b) { + if (getItemViewType(a) != getItemViewType(b)) { + return false; + } + if (a.getConversation().getMode() == Conversation.MODE_MULTI + && a.getStatus() == Message.STATUS_RECEIVED) { + final var occupantIdA = a.getOccupantId(); + final var occupantIdB = b.getOccupantId(); + if (occupantIdA != null && occupantIdB != null) { + if (!occupantIdA.equals(occupantIdB)) { + return false; + } + } + final var counterPartA = a.getCounterpart(); + final var counterPartB = b.getCounterpart(); + if (counterPartA == null || !counterPartA.equals(counterPartB)) { + return false; + } + } + return b.getTimeSent() - a.getTimeSent() <= Config.MESSAGE_MERGE_WINDOW; + } + private boolean showDetailedReaction(final Message message, final String emoji) { final var c = message.getConversation(); if (c instanceof Conversation conversation && c.getMode() == Conversational.MODE_MULTI) { @@ -1300,6 +1384,7 @@ public class MessageAdapter extends ArrayAdapter { private static class ViewHolder { + private ConstraintLayout root; public MaterialButton load_more_messages; public ImageView edit_indicator; public RelativeLayout audioPlayer; diff --git a/src/main/res/layout/item_message_received.xml b/src/main/res/layout/item_message_received.xml index 60c665ffe0fc570fb0bf8e7a2bcb3a00f2400440..0df590d2446321be49c9328ed0b61802c6a314e6 100644 --- a/src/main/res/layout/item_message_received.xml +++ b/src/main/res/layout/item_message_received.xml @@ -6,13 +6,13 @@ + android:paddingHorizontal="@dimen/bubble_horizontal_padding" + android:paddingVertical="@dimen/bubble_vertical_padding"> + android:paddingHorizontal="@dimen/bubble_horizontal_padding" + android:paddingVertical="@dimen/bubble_vertical_padding"> 128dp 96dp 24dp + + 8dp + 4dp + 1dp + 48dp