@@ -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;
@@ -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<Message> {
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<Message> {
}
@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> {
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<Message> {
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<Message> {
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<Message> {
} 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<Message> {
}
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<Message> {
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<Message> {
private static class ViewHolder {
+ private ConstraintLayout root;
public MaterialButton load_more_messages;
public ImageView edit_indicator;
public RelativeLayout audioPlayer;
@@ -6,13 +6,13 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingHorizontal="8dp"
- android:paddingVertical="4dp">
+ android:paddingHorizontal="@dimen/bubble_horizontal_padding"
+ android:paddingVertical="@dimen/bubble_vertical_padding">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/message_photo"
- android:layout_width="48dp"
- android:layout_height="48dp"
+ android:layout_width="@dimen/bubble_avatar_size"
+ android:layout_height="@dimen/bubble_avatar_size"
android:scaleType="fitXY"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/message_box"
@@ -27,7 +27,7 @@
android:background="@drawable/background_message_bubble"
android:backgroundTint="?colorTertiaryContainer"
android:longClickable="true"
- android:minHeight="48dp"
+ android:minHeight="@dimen/bubble_avatar_size"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
@@ -5,13 +5,13 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingHorizontal="8dp"
- android:paddingVertical="4dp">
+ android:paddingHorizontal="@dimen/bubble_horizontal_padding"
+ android:paddingVertical="@dimen/bubble_vertical_padding">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/message_photo"
- android:layout_width="48dp"
- android:layout_height="48dp"
+ android:layout_width="@dimen/bubble_avatar_size"
+ android:layout_height="@dimen/bubble_avatar_size"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="@id/message_box"
app:layout_constraintEnd_toEndOf="parent"
@@ -26,7 +26,7 @@
android:background="@drawable/background_message_bubble"
android:backgroundTint="?colorSecondaryContainer"
android:longClickable="true"
- android:minHeight="48dp"
+ android:minHeight="@dimen/bubble_avatar_size"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/message_photo"
app:layout_constraintHorizontal_bias="1.0"
@@ -42,4 +42,9 @@
<dimen name="local_video_preview_height">128dp</dimen>
<dimen name="local_video_preview_width">96dp</dimen>
<dimen name="rtp_session_duration_top_margin">24dp</dimen>
+
+ <dimen name="bubble_horizontal_padding">8dp</dimen>
+ <dimen name="bubble_vertical_padding">4dp</dimen>
+ <dimen name="bubble_vertical_padding_minimum">1dp</dimen>
+ <dimen name="bubble_avatar_size">48dp</dimen>
</resources>