use data binding in MessageAdapter

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java | 953 
1 file changed, 593 insertions(+), 360 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -15,6 +15,7 @@ import android.text.style.ForegroundColorSpan;
 import android.text.style.RelativeSizeSpan;
 import android.text.style.StyleSpan;
 import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -33,6 +34,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.widget.ImageViewCompat;
+import androidx.databinding.DataBindingUtil;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.chip.ChipGroup;
 import com.google.android.material.color.MaterialColors;
@@ -45,6 +47,11 @@ import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
+import eu.siacs.conversations.databinding.ItemMessageDateBubbleBinding;
+import eu.siacs.conversations.databinding.ItemMessageReceivedBinding;
+import eu.siacs.conversations.databinding.ItemMessageRtpSessionBinding;
+import eu.siacs.conversations.databinding.ItemMessageSentBinding;
+import eu.siacs.conversations.databinding.ItemMessageStatusBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
@@ -175,15 +182,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     }
 
     private void displayStatus(
-            final ViewHolder viewHolder,
+            final BubbleMessageItemViewHolder viewHolder,
             final Message message,
             final int type,
             final BubbleColor bubbleColor) {
         final int mergedStatus = message.getStatus();
         final boolean error;
-        if (viewHolder.indicatorReceived != null) {
-            viewHolder.indicatorReceived.setVisibility(View.GONE);
-        }
         final Transferable transferable = message.getTransferable();
         final boolean multiReceived =
                 message.getConversation().getMode() == Conversation.MODE_MULTI
@@ -207,32 +211,35 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             fileSize = null;
             error = message.getStatus() == Message.STATUS_SEND_FAILED;
         }
-        if (type == SENT) {
+        if (type == SENT && viewHolder instanceof EndBubbleMessageItemViewHolder endViewHolder) {
             final @DrawableRes Integer receivedIndicator =
                     getMessageStatusAsDrawable(message, mergedStatus);
             if (receivedIndicator == null) {
-                viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
+                endViewHolder.indicatorReceived().setVisibility(View.INVISIBLE);
             } else {
-                viewHolder.indicatorReceived.setImageResource(receivedIndicator);
+                endViewHolder.indicatorReceived().setImageResource(receivedIndicator);
                 if (mergedStatus == Message.STATUS_SEND_FAILED) {
-                    setImageTintError(viewHolder.indicatorReceived);
+                    setImageTintError(endViewHolder.indicatorReceived());
                 } else {
-                    setImageTint(viewHolder.indicatorReceived, bubbleColor);
+                    setImageTint(endViewHolder.indicatorReceived(), bubbleColor);
                 }
-                viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
+                endViewHolder.indicatorReceived().setVisibility(View.VISIBLE);
             }
         }
         final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
 
         if (error && type == SENT) {
-            viewHolder.time.setTextColor(
-                    MaterialColors.getColor(
-                            viewHolder.time, com.google.android.material.R.attr.colorError));
+            viewHolder
+                    .time()
+                    .setTextColor(
+                            MaterialColors.getColor(
+                                    viewHolder.time(),
+                                    com.google.android.material.R.attr.colorError));
         } else {
-            setTextColor(viewHolder.time, bubbleColor);
+            setTextColor(viewHolder.time(), bubbleColor);
         }
         if (message.getEncryption() == Message.ENCRYPTION_NONE) {
-            viewHolder.indicator.setVisibility(View.GONE);
+            viewHolder.indicator().setVisibility(View.GONE);
         } else {
             boolean verified = false;
             if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
@@ -246,29 +253,27 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 }
             }
             if (verified) {
-                viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
+                viewHolder.indicator().setImageResource(R.drawable.ic_verified_user_24dp);
             } else {
-                viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
+                viewHolder.indicator().setImageResource(R.drawable.ic_lock_24dp);
             }
             if (error && type == SENT) {
-                setImageTintError(viewHolder.indicator);
+                setImageTintError(viewHolder.indicator());
             } else {
-                setImageTint(viewHolder.indicator, bubbleColor);
+                setImageTint(viewHolder.indicator(), bubbleColor);
             }
-            viewHolder.indicator.setVisibility(View.VISIBLE);
+            viewHolder.indicator().setVisibility(View.VISIBLE);
         }
 
-        if (viewHolder.edit_indicator != null) {
-            if (message.edited()) {
-                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
-                if (error && type == SENT) {
-                    setImageTintError(viewHolder.edit_indicator);
-                } else {
-                    setImageTint(viewHolder.edit_indicator, bubbleColor);
-                }
+        if (message.edited()) {
+            viewHolder.editIndicator().setVisibility(View.VISIBLE);
+            if (error && type == SENT) {
+                setImageTintError(viewHolder.editIndicator());
             } else {
-                viewHolder.edit_indicator.setVisibility(View.GONE);
+                setImageTint(viewHolder.editIndicator(), bubbleColor);
             }
+        } else {
+            viewHolder.editIndicator().setVisibility(View.GONE);
         }
 
         final String formattedTime =
@@ -305,7 +310,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             }
         }
         final var timeInfo = timeInfoBuilder.build();
-        viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
+        viewHolder.time().setText(Joiner.on(" \u00B7 ").join(timeInfo));
     }
 
     public static @DrawableRes Integer getMessageStatusAsDrawable(
@@ -354,29 +359,34 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     }
 
     private void displayInfoMessage(
-            ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
-        viewHolder.download_button.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.messageBody.setVisibility(View.VISIBLE);
-        viewHolder.messageBody.setText(text);
-        viewHolder.messageBody.setTextColor(
-                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
-        viewHolder.messageBody.setTextIsSelectable(false);
+            BubbleMessageItemViewHolder viewHolder,
+            CharSequence text,
+            final BubbleColor bubbleColor) {
+        viewHolder.downloadButton().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.messageBody().setVisibility(View.VISIBLE);
+        viewHolder.messageBody().setText(text);
+        viewHolder
+                .messageBody()
+                .setTextColor(bubbleToOnSurfaceVariant(viewHolder.messageBody(), bubbleColor));
+        viewHolder.messageBody().setTextIsSelectable(false);
     }
 
     private void displayEmojiMessage(
-            final ViewHolder viewHolder, final String body, final BubbleColor bubbleColor) {
-        viewHolder.download_button.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.messageBody.setVisibility(View.VISIBLE);
-        setTextColor(viewHolder.messageBody, bubbleColor);
+            final BubbleMessageItemViewHolder viewHolder,
+            final String body,
+            final BubbleColor bubbleColor) {
+        viewHolder.downloadButton().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.messageBody().setVisibility(View.VISIBLE);
+        setTextColor(viewHolder.messageBody(), bubbleColor);
         final Spannable span = new SpannableString(body);
         float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f;
         span.setSpan(
                 new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-        viewHolder.messageBody.setText(span);
+        viewHolder.messageBody().setText(span);
     }
 
     private void applyQuoteSpan(
@@ -462,14 +472,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     }
 
     private void displayTextMessage(
-            final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
-        viewHolder.download_button.setVisibility(View.GONE);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.messageBody.setVisibility(View.VISIBLE);
-        setTextColor(viewHolder.messageBody, bubbleColor);
-        setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
-        viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+            final BubbleMessageItemViewHolder viewHolder,
+            final Message message,
+            final BubbleColor bubbleColor) {
+        viewHolder.downloadButton().setVisibility(View.GONE);
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.messageBody().setVisibility(View.VISIBLE);
+        setTextColor(viewHolder.messageBody(), bubbleColor);
+        setTextSize(viewHolder.messageBody(), this.bubbleDesign.largeFont);
+        viewHolder.messageBody().setTypeface(null, Typeface.NORMAL);
 
         if (message.getBody() != null) {
             final String nick = UIHelper.getMessageDisplayName(message);
@@ -485,7 +497,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             if (hasMeCommand) {
                 body.replace(0, Message.ME_COMMAND.length(), String.format("%s ", nick));
             }
-            boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
+            boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody(), body, bubbleColor);
             if (!message.isPrivateMessage()) {
                 if (hasMeCommand) {
                     body.setSpan(
@@ -519,7 +531,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 }
                 body.setSpan(
                         new ForegroundColorSpan(
-                                bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                                bubbleToOnSurfaceVariant(viewHolder.messageBody(), bubbleColor)),
                         0,
                         privateMarkerIndex,
                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -563,83 +575,94 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                 }
             }
 
-            StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
+            StylingHelper.format(body, viewHolder.messageBody().getCurrentTextColor());
             MyLinkify.addLinks(body, true);
             if (highlightedTerm != null) {
-                StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
+                StylingHelper.highlight(viewHolder.messageBody(), body, highlightedTerm);
             }
-            viewHolder.messageBody.setAutoLinkMask(0);
-            viewHolder.messageBody.setText(body);
-            viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
+            viewHolder.messageBody().setAutoLinkMask(0);
+            viewHolder.messageBody().setText(body);
+            viewHolder.messageBody().setMovementMethod(ClickableMovementMethod.getInstance());
         } else {
-            viewHolder.messageBody.setText("");
-            viewHolder.messageBody.setTextIsSelectable(false);
+            viewHolder.messageBody().setText("");
+            viewHolder.messageBody().setTextIsSelectable(false);
         }
     }
 
     private void displayDownloadableMessage(
-            ViewHolder viewHolder,
+            final BubbleMessageItemViewHolder viewHolder,
             final Message message,
-            String text,
+            final String text,
             final BubbleColor bubbleColor) {
         toggleWhisperInfo(viewHolder, message, bubbleColor);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.download_button.setVisibility(View.VISIBLE);
-        viewHolder.download_button.setText(text);
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.downloadButton().setVisibility(View.VISIBLE);
+        viewHolder.downloadButton().setText(text);
         final var attachment = Attachment.of(message);
         final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
-        viewHolder.download_button.setIconResource(imageResource);
-        viewHolder.download_button.setOnClickListener(
-                v -> ConversationFragment.downloadFile(activity, message));
+        viewHolder.downloadButton().setIconResource(imageResource);
+        viewHolder
+                .downloadButton()
+                .setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
     }
 
     private void displayOpenableMessage(
-            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
+            final BubbleMessageItemViewHolder viewHolder,
+            final Message message,
+            final BubbleColor bubbleColor) {
         toggleWhisperInfo(viewHolder, message, bubbleColor);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.download_button.setVisibility(View.VISIBLE);
-        viewHolder.download_button.setText(
-                activity.getString(
-                        R.string.open_x_file,
-                        UIHelper.getFileDescriptionString(activity, message)));
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.downloadButton().setVisibility(View.VISIBLE);
+        viewHolder
+                .downloadButton()
+                .setText(
+                        activity.getString(
+                                R.string.open_x_file,
+                                UIHelper.getFileDescriptionString(activity, message)));
         final var attachment = Attachment.of(message);
         final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
-        viewHolder.download_button.setIconResource(imageResource);
-        viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
+        viewHolder.downloadButton().setIconResource(imageResource);
+        viewHolder.downloadButton().setOnClickListener(v -> openDownloadable(message));
     }
 
     private void displayLocationMessage(
-            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
+            final BubbleMessageItemViewHolder viewHolder,
+            final Message message,
+            final BubbleColor bubbleColor) {
         toggleWhisperInfo(viewHolder, message, bubbleColor);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.download_button.setVisibility(View.VISIBLE);
-        viewHolder.download_button.setText(R.string.show_location);
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.downloadButton().setVisibility(View.VISIBLE);
+        viewHolder.downloadButton().setText(R.string.show_location);
         final var attachment = Attachment.of(message);
         final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
-        viewHolder.download_button.setIconResource(imageResource);
-        viewHolder.download_button.setOnClickListener(v -> showLocation(message));
+        viewHolder.downloadButton().setIconResource(imageResource);
+        viewHolder.downloadButton().setOnClickListener(v -> showLocation(message));
     }
 
     private void displayAudioMessage(
-            ViewHolder viewHolder, Message message, final BubbleColor bubbleColor) {
+            final BubbleMessageItemViewHolder viewHolder,
+            Message message,
+            final BubbleColor bubbleColor) {
         toggleWhisperInfo(viewHolder, message, bubbleColor);
-        viewHolder.image.setVisibility(View.GONE);
-        viewHolder.download_button.setVisibility(View.GONE);
-        final RelativeLayout audioPlayer = viewHolder.audioPlayer;
+        viewHolder.image().setVisibility(View.GONE);
+        viewHolder.downloadButton().setVisibility(View.GONE);
+        final RelativeLayout audioPlayer = viewHolder.audioPlayer();
         audioPlayer.setVisibility(View.VISIBLE);
         AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
         this.audioPlayer.init(audioPlayer, message);
     }
 
     private void displayMediaPreviewMessage(
-            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
+            final BubbleMessageItemViewHolder viewHolder,
+            final Message message,
+            final BubbleColor bubbleColor) {
         toggleWhisperInfo(viewHolder, message, bubbleColor);
-        viewHolder.download_button.setVisibility(View.GONE);
-        viewHolder.audioPlayer.setVisibility(View.GONE);
-        viewHolder.image.setVisibility(View.VISIBLE);
+        viewHolder.downloadButton().setVisibility(View.GONE);
+        viewHolder.audioPlayer().setVisibility(View.GONE);
+        viewHolder.image().setVisibility(View.VISIBLE);
         final FileParams params = message.getFileParams();
         final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
         final int scaledW;
@@ -659,13 +682,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         }
         final LinearLayout.LayoutParams layoutParams =
                 new LinearLayout.LayoutParams(scaledW, scaledH);
-        viewHolder.image.setLayoutParams(layoutParams);
-        activity.loadBitmap(message, viewHolder.image);
-        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
+        viewHolder.image().setLayoutParams(layoutParams);
+        activity.loadBitmap(message, viewHolder.image());
+        viewHolder.image().setOnClickListener(v -> openDownloadable(message));
     }
 
     private void toggleWhisperInfo(
-            ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
+            final BubbleMessageItemViewHolder viewHolder,
+            final Message message,
+            final BubbleColor bubbleColor) {
         if (message.isPrivateMessage()) {
             final String privateMarker;
             if (message.getStatus() <= Message.STATUS_RECEIVED) {
@@ -680,7 +705,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             final SpannableString body = new SpannableString(privateMarker);
             body.setSpan(
                     new ForegroundColorSpan(
-                            bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
+                            bubbleToOnSurfaceVariant(viewHolder.messageBody(), bubbleColor)),
                     0,
                     privateMarker.length(),
                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -689,14 +714,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     0,
                     privateMarker.length(),
                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-            viewHolder.messageBody.setText(body);
-            viewHolder.messageBody.setVisibility(View.VISIBLE);
+            viewHolder.messageBody().setText(body);
+            viewHolder.messageBody().setVisibility(View.VISIBLE);
         } else {
-            viewHolder.messageBody.setVisibility(View.GONE);
+            viewHolder.messageBody().setVisibility(View.GONE);
         }
     }
 
-    private void loadMoreMessages(Conversation conversation) {
+    private void loadMoreMessages(final Conversation conversation) {
         conversation.setLastClearHistory(0, null);
         activity.xmppConnectionService.updateConversation(conversation);
         conversation.setHasMessagesLeftOnServer(true);
@@ -722,88 +747,96 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         }
     }
 
+    private MessageItemViewHolder getViewHolder(
+            final View view, final @NonNull ViewGroup parent, final int type) {
+        if (view != null && view.getTag() instanceof MessageItemViewHolder messageItemViewHolder) {
+            return messageItemViewHolder;
+        } else {
+            final MessageItemViewHolder viewHolder =
+                    switch (type) {
+                        case RTP_SESSION ->
+                                new RtpSessionMessageItemViewHolder(
+                                        DataBindingUtil.inflate(
+                                                LayoutInflater.from(parent.getContext()),
+                                                R.layout.item_message_rtp_session,
+                                                parent,
+                                                false));
+                        case DATE_SEPARATOR ->
+                                new DateSeperatorMessageItemViewHolder(
+                                        DataBindingUtil.inflate(
+                                                LayoutInflater.from(parent.getContext()),
+                                                R.layout.item_message_date_bubble,
+                                                parent,
+                                                false));
+                        case STATUS ->
+                                new StatusMessageItemViewHolder(
+                                        DataBindingUtil.inflate(
+                                                LayoutInflater.from(parent.getContext()),
+                                                R.layout.item_message_status,
+                                                parent,
+                                                false));
+                        case SENT ->
+                                new EndBubbleMessageItemViewHolder(
+                                        DataBindingUtil.inflate(
+                                                LayoutInflater.from(parent.getContext()),
+                                                R.layout.item_message_sent,
+                                                parent,
+                                                false));
+                        case RECEIVED ->
+                                new StartBubbleMessageItemViewHolder(
+                                        DataBindingUtil.inflate(
+                                                LayoutInflater.from(parent.getContext()),
+                                                R.layout.item_message_received,
+                                                parent,
+                                                false));
+                        default -> throw new AssertionError("Unable to create ViewHolder for type");
+                    };
+            viewHolder.itemView.setTag(viewHolder);
+            return viewHolder;
+        }
+    }
+
     @NonNull
     @Override
     public View getView(final int position, View view, final @NonNull ViewGroup parent) {
         final Message message = getItem(position);
+        final int type = getItemViewType(message);
+        final MessageItemViewHolder viewHolder = getViewHolder(view, parent, type);
+
+        if (type == DATE_SEPARATOR
+                && viewHolder instanceof DateSeperatorMessageItemViewHolder messageItemViewHolder) {
+            return render(message, messageItemViewHolder);
+        }
+
+        if (type == RTP_SESSION
+                && viewHolder instanceof RtpSessionMessageItemViewHolder messageItemViewHolder) {
+            return render(message, messageItemViewHolder);
+        }
+
+        if (type == STATUS
+                && viewHolder instanceof StatusMessageItemViewHolder messageItemViewHolder) {
+            return render(message, messageItemViewHolder);
+        }
+
+        if ((type == SENT || type == RECEIVED)
+                && viewHolder instanceof BubbleMessageItemViewHolder messageItemViewHolder) {
+            // TODO: type is represented by the class of viewHolder. we can get rid of that
+            return render(position, message, type, messageItemViewHolder);
+        }
+
+        throw new AssertionError();
+    }
+
+    private View render(
+            final int position,
+            final Message message,
+            final int type,
+            final BubbleMessageItemViewHolder viewHolder) {
         final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
         final boolean isInValidSession =
                 message.isValidInSession() && (!omemoEncryption || message.isTrusted());
         final Conversational conversation = message.getConversation();
         final Account account = conversation.getAccount();
-        final int type = getItemViewType(message);
-        ViewHolder viewHolder;
-        if (view == null) {
-            viewHolder = new ViewHolder();
-            switch (type) {
-                case DATE_SEPARATOR:
-                    view =
-                            activity.getLayoutInflater()
-                                    .inflate(R.layout.item_message_date_bubble, parent, false);
-                    viewHolder.status_message = view.findViewById(R.id.message_body);
-                    viewHolder.message_box = view.findViewById(R.id.message_box);
-                    break;
-                case RTP_SESSION:
-                    view =
-                            activity.getLayoutInflater()
-                                    .inflate(R.layout.item_message_rtp_session, parent, false);
-                    viewHolder.status_message = view.findViewById(R.id.message_body);
-                    viewHolder.message_box = view.findViewById(R.id.message_box);
-                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
-                    break;
-                case SENT:
-                    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);
-                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
-                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
-                    viewHolder.image = view.findViewById(R.id.message_image);
-                    viewHolder.messageBody = view.findViewById(R.id.message_body);
-                    viewHolder.time = view.findViewById(R.id.message_time);
-                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
-                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
-                    viewHolder.reactions = view.findViewById(R.id.reactions);
-                    break;
-                case RECEIVED:
-                    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);
-                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
-                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
-                    viewHolder.image = view.findViewById(R.id.message_image);
-                    viewHolder.messageBody = view.findViewById(R.id.message_body);
-                    viewHolder.time = view.findViewById(R.id.message_time);
-                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
-                    viewHolder.encryption = view.findViewById(R.id.message_encryption);
-                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
-                    viewHolder.reactions = view.findViewById(R.id.reactions);
-                    break;
-                case STATUS:
-                    view =
-                            activity.getLayoutInflater()
-                                    .inflate(R.layout.item_message_status, parent, false);
-                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
-                    viewHolder.status_message = view.findViewById(R.id.status_message);
-                    viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
-                    break;
-                default:
-                    throw new AssertionError("Unknown view type");
-            }
-            view.setTag(viewHolder);
-        } else {
-            viewHolder = (ViewHolder) view.getTag();
-            if (viewHolder == null) {
-                return view;
-            }
-        }
 
         final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
         final BubbleColor bubbleColor;
@@ -817,149 +850,46 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
         }
 
-        if (type == DATE_SEPARATOR) {
-            if (UIHelper.today(message.getTimeSent())) {
-                viewHolder.status_message.setText(R.string.today);
-            } else if (UIHelper.yesterday(message.getTimeSent())) {
-                viewHolder.status_message.setText(R.string.yesterday);
-            } else {
-                viewHolder.status_message.setText(
-                        DateUtils.formatDateTime(
-                                activity,
-                                message.getTimeSent(),
-                                DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
-            }
-            if (colorfulBackground) {
-                setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
-                setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
-            } else {
-                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
-                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
-            }
-            return view;
-        } else if (type == RTP_SESSION) {
-            final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
-            final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
-            final long duration = rtpSessionStatus.duration;
-            if (received) {
-                if (duration > 0) {
-                    viewHolder.status_message.setText(
-                            activity.getString(
-                                    R.string.incoming_call_duration_timestamp,
-                                    TimeFrameUtils.resolve(activity, duration),
-                                    UIHelper.readableTimeDifferenceFull(
-                                            activity, message.getTimeSent())));
-                } else if (rtpSessionStatus.successful) {
-                    viewHolder.status_message.setText(R.string.incoming_call);
-                } else {
-                    viewHolder.status_message.setText(
-                            activity.getString(
-                                    R.string.missed_call_timestamp,
-                                    UIHelper.readableTimeDifferenceFull(
-                                            activity, message.getTimeSent())));
-                }
-            } else {
-                if (duration > 0) {
-                    viewHolder.status_message.setText(
-                            activity.getString(
-                                    R.string.outgoing_call_duration_timestamp,
-                                    TimeFrameUtils.resolve(activity, duration),
-                                    UIHelper.readableTimeDifferenceFull(
-                                            activity, message.getTimeSent())));
-                } else {
-                    viewHolder.status_message.setText(
-                            activity.getString(
-                                    R.string.outgoing_call_timestamp,
-                                    UIHelper.readableTimeDifferenceFull(
-                                            activity, message.getTimeSent())));
-                }
-            }
-            if (colorfulBackground) {
-                setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
-                setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
-                setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
-            } else {
-                setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
-                setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
-                setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
-            }
-            viewHolder.indicatorReceived.setImageResource(
-                    RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
-            return view;
-        } else if (type == STATUS) {
-            if ("LOAD_MORE".equals(message.getBody())) {
-                viewHolder.status_message.setVisibility(View.GONE);
-                viewHolder.contact_picture.setVisibility(View.GONE);
-                viewHolder.load_more_messages.setVisibility(View.VISIBLE);
-                viewHolder.load_more_messages.setOnClickListener(
-                        v -> loadMoreMessages((Conversation) message.getConversation()));
-            } else {
-                viewHolder.status_message.setVisibility(View.VISIBLE);
-                viewHolder.load_more_messages.setVisibility(View.GONE);
-                viewHolder.status_message.setText(message.getBody());
-                boolean showAvatar;
-                if (conversation.getMode() == Conversation.MODE_SINGLE) {
-                    showAvatar = true;
-                    AvatarWorkerTask.loadAvatar(
-                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
-                } else if (message.getCounterpart() != null
-                        || message.getTrueCounterpart() != null
-                        || (message.getCounterparts() != null
-                                && !message.getCounterparts().isEmpty())) {
-                    showAvatar = true;
-                    AvatarWorkerTask.loadAvatar(
-                            message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
-                } else {
-                    showAvatar = false;
-                }
-                if (showAvatar) {
-                    viewHolder.contact_picture.setAlpha(0.5f);
-                    viewHolder.contact_picture.setVisibility(View.VISIBLE);
-                } else {
-                    viewHolder.contact_picture.setVisibility(View.GONE);
-                }
-            }
-            return view;
+        final var mergeIntoTop = mergeIntoTop(position, message);
+        final var mergeIntoBottom = mergeIntoBottom(position, message);
+        final var showAvatar =
+                bubbleDesign.showAvatars
+                        || (type == RECEIVED
+                                && message.getConversation().getMode() == Conversation.MODE_MULTI);
+        setBubblePadding(viewHolder.root(), mergeIntoTop, mergeIntoBottom);
+        if (showAvatar) {
+            final var requiresAvatar = type == SENT ? !mergeIntoBottom : !mergeIntoTop;
+            setRequiresAvatar(viewHolder, requiresAvatar);
+            AvatarWorkerTask.loadAvatar(message, viewHolder.contactPicture(), R.dimen.avatar);
         } else {
-            // sent and received bubbles
-            final var mergeIntoTop = mergeIntoTop(position, message);
-            final var mergeIntoBottom = mergeIntoBottom(position, message);
-            final var showAvatar =
-                    bubbleDesign.showAvatars
-                            || (type == RECEIVED
-                                    && message.getConversation().getMode()
-                                            == Conversation.MODE_MULTI);
-            setBubblePadding(viewHolder.root, mergeIntoTop, mergeIntoBottom);
-            if (showAvatar) {
-                final var requiresAvatar = type == SENT ? !mergeIntoBottom : !mergeIntoTop;
-                setRequiresAvatar(viewHolder, requiresAvatar);
-                AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
-            } else {
-                viewHolder.contact_picture.setVisibility(View.GONE);
-            }
-            setAvatarDistance(viewHolder.message_box, type, showAvatar);
-            viewHolder.message_box.setClipToOutline(true);
+            viewHolder.contactPicture().setVisibility(View.GONE);
         }
-
-        resetClickListener(viewHolder.message_box, viewHolder.messageBody);
-
-        viewHolder.contact_picture.setOnClickListener(
-                v -> {
-                    if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
-                        MessageAdapter.this.mOnContactPictureClickedListener
-                                .onContactPictureClicked(message);
-                    }
-                });
-        viewHolder.contact_picture.setOnLongClickListener(
-                v -> {
-                    if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
-                        MessageAdapter.this.mOnContactPictureLongClickedListener
-                                .onContactPictureLongClicked(v, message);
-                        return true;
-                    } else {
-                        return false;
-                    }
-                });
+        setAvatarDistance(viewHolder.messageBox(), type, showAvatar);
+        viewHolder.messageBox().setClipToOutline(true);
+
+        resetClickListener(viewHolder.messageBox(), viewHolder.messageBody());
+
+        viewHolder
+                .contactPicture()
+                .setOnClickListener(
+                        v -> {
+                            if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
+                                MessageAdapter.this.mOnContactPictureClickedListener
+                                        .onContactPictureClicked(message);
+                            }
+                        });
+        viewHolder
+                .contactPicture()
+                .setOnLongClickListener(
+                        v -> {
+                            if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
+                                MessageAdapter.this.mOnContactPictureLongClickedListener
+                                        .onContactPictureLongClicked(v, message);
+                                return true;
+                            } else {
+                                return false;
+                            }
+                        });
 
         final Transferable transferable = message.getTransferable();
         final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
@@ -1017,8 +947,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             } else {
                 displayInfoMessage(
                         viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
-                viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
-                viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
+                viewHolder.messageBox().setOnClickListener(this::promptOpenKeychainInstall);
+                viewHolder.messageBody().setOnClickListener(this::promptOpenKeychainInstall);
             }
         } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
             displayInfoMessage(
@@ -1061,38 +991,153 @@ public class MessageAdapter extends ArrayAdapter<Message> {
             }
         }
 
-        setBackgroundTint(viewHolder.message_box, bubbleColor);
-        setTextColor(viewHolder.messageBody, bubbleColor);
+        setBackgroundTint(viewHolder.messageBox(), bubbleColor);
+        setTextColor(viewHolder.messageBody(), bubbleColor);
 
-        if (type == RECEIVED) {
-            setTextColor(viewHolder.encryption, bubbleColor);
+        if (type == RECEIVED
+                && viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) {
+            setTextColor(startViewHolder.encryption(), bubbleColor);
             if (isInValidSession) {
-                viewHolder.encryption.setVisibility(View.GONE);
+                startViewHolder.encryption().setVisibility(View.GONE);
             } else {
-                viewHolder.encryption.setVisibility(View.VISIBLE);
+                startViewHolder.encryption().setVisibility(View.VISIBLE);
                 if (omemoEncryption && !message.isTrusted()) {
-                    viewHolder.encryption.setText(R.string.not_trusted);
+                    startViewHolder.encryption().setText(R.string.not_trusted);
                 } else {
-                    viewHolder.encryption.setText(
-                            CryptoHelper.encryptionTypeToText(message.getEncryption()));
+                    startViewHolder
+                            .encryption()
+                            .setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
                 }
             }
             BindingAdapters.setReactionsOnReceived(
-                    viewHolder.reactions,
+                    viewHolder.reactions(),
                     message.getAggregatedReactions(),
                     reactions -> sendReactions(message, reactions),
                     emoji -> showDetailedReaction(message, emoji),
                     () -> addReaction(message));
         } else if (type == SENT) {
             BindingAdapters.setReactionsOnSent(
-                    viewHolder.reactions,
+                    viewHolder.reactions(),
                     message.getAggregatedReactions(),
                     reactions -> sendReactions(message, reactions),
                     emoji -> showDetailedReaction(message, emoji));
         }
 
         displayStatus(viewHolder, message, type, bubbleColor);
-        return view;
+        return viewHolder.root();
+    }
+
+    private View render(
+            final Message message, final DateSeperatorMessageItemViewHolder viewHolder) {
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        if (UIHelper.today(message.getTimeSent())) {
+            viewHolder.binding.messageBody.setText(R.string.today);
+        } else if (UIHelper.yesterday(message.getTimeSent())) {
+            viewHolder.binding.messageBody.setText(R.string.yesterday);
+        } else {
+            viewHolder.binding.messageBody.setText(
+                    DateUtils.formatDateTime(
+                            activity,
+                            message.getTimeSent(),
+                            DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
+        }
+        if (colorfulBackground) {
+            setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.PRIMARY);
+            setTextColor(viewHolder.binding.messageBody, BubbleColor.PRIMARY);
+        } else {
+            setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SURFACE_HIGH);
+            setTextColor(viewHolder.binding.messageBody, BubbleColor.SURFACE_HIGH);
+        }
+        return viewHolder.binding.getRoot();
+    }
+
+    private View render(final Message message, final RtpSessionMessageItemViewHolder viewHolder) {
+        final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
+        final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
+        final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
+        final long duration = rtpSessionStatus.duration;
+        if (received) {
+            if (duration > 0) {
+                viewHolder.binding.messageBody.setText(
+                        activity.getString(
+                                R.string.incoming_call_duration_timestamp,
+                                TimeFrameUtils.resolve(activity, duration),
+                                UIHelper.readableTimeDifferenceFull(
+                                        activity, message.getTimeSent())));
+            } else if (rtpSessionStatus.successful) {
+                viewHolder.binding.messageBody.setText(R.string.incoming_call);
+            } else {
+                viewHolder.binding.messageBody.setText(
+                        activity.getString(
+                                R.string.missed_call_timestamp,
+                                UIHelper.readableTimeDifferenceFull(
+                                        activity, message.getTimeSent())));
+            }
+        } else {
+            if (duration > 0) {
+                viewHolder.binding.messageBody.setText(
+                        activity.getString(
+                                R.string.outgoing_call_duration_timestamp,
+                                TimeFrameUtils.resolve(activity, duration),
+                                UIHelper.readableTimeDifferenceFull(
+                                        activity, message.getTimeSent())));
+            } else {
+                viewHolder.binding.messageBody.setText(
+                        activity.getString(
+                                R.string.outgoing_call_timestamp,
+                                UIHelper.readableTimeDifferenceFull(
+                                        activity, message.getTimeSent())));
+            }
+        }
+        if (colorfulBackground) {
+            setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SECONDARY);
+            setTextColor(viewHolder.binding.messageBody, BubbleColor.SECONDARY);
+            setImageTint(viewHolder.binding.indicatorReceived, BubbleColor.SECONDARY);
+        } else {
+            setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SURFACE_HIGH);
+            setTextColor(viewHolder.binding.messageBody, BubbleColor.SURFACE_HIGH);
+            setImageTint(viewHolder.binding.indicatorReceived, BubbleColor.SURFACE_HIGH);
+        }
+        viewHolder.binding.indicatorReceived.setImageResource(
+                RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
+        return viewHolder.binding.getRoot();
+    }
+
+    private View render(final Message message, final StatusMessageItemViewHolder viewHolder) {
+        final var conversation = message.getConversation();
+        if ("LOAD_MORE".equals(message.getBody())) {
+            viewHolder.binding.statusMessage.setVisibility(View.GONE);
+            viewHolder.binding.messagePhoto.setVisibility(View.GONE);
+            viewHolder.binding.loadMoreMessages.setVisibility(View.VISIBLE);
+            viewHolder.binding.loadMoreMessages.setOnClickListener(
+                    v -> loadMoreMessages((Conversation) message.getConversation()));
+        } else {
+            viewHolder.binding.statusMessage.setVisibility(View.VISIBLE);
+            viewHolder.binding.loadMoreMessages.setVisibility(View.GONE);
+            viewHolder.binding.statusMessage.setText(message.getBody());
+            boolean showAvatar;
+            if (conversation.getMode() == Conversation.MODE_SINGLE) {
+                showAvatar = true;
+                AvatarWorkerTask.loadAvatar(
+                        message, viewHolder.binding.messagePhoto, R.dimen.avatar_on_status_message);
+            } else if (message.getCounterpart() != null
+                    || message.getTrueCounterpart() != null
+                    || (message.getCounterparts() != null
+                            && !message.getCounterparts().isEmpty())) {
+                showAvatar = true;
+                AvatarWorkerTask.loadAvatar(
+                        message, viewHolder.binding.messagePhoto, R.dimen.avatar_on_status_message);
+            } else {
+                showAvatar = false;
+            }
+            if (showAvatar) {
+                viewHolder.binding.messagePhoto.setAlpha(0.5f);
+                viewHolder.binding.messagePhoto.setVisibility(View.VISIBLE);
+            } else {
+                viewHolder.binding.messagePhoto.setVisibility(View.GONE);
+            }
+        }
+        return viewHolder.binding.getRoot();
     }
 
     private void setAvatarDistance(
@@ -1138,20 +1183,21 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         root.setPadding(horizontal, top, horizontal, bottom);
     }
 
-    private void setRequiresAvatar(final ViewHolder viewHolder, final boolean requiresAvatar) {
-        final var layoutParams = viewHolder.contact_picture.getLayoutParams();
+    private void setRequiresAvatar(
+            final BubbleMessageItemViewHolder viewHolder, final boolean requiresAvatar) {
+        final var layoutParams = viewHolder.contactPicture().getLayoutParams();
         if (requiresAvatar) {
-            final var resources = viewHolder.contact_picture.getResources();
+            final var resources = viewHolder.contactPicture().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);
+            viewHolder.contactPicture().setVisibility(View.VISIBLE);
+            viewHolder.messageBox().setMinimumHeight(avatarSize);
         } else {
             layoutParams.height = 0;
-            viewHolder.contact_picture.setVisibility(View.INVISIBLE);
-            viewHolder.message_box.setMinimumHeight(0);
+            viewHolder.contactPicture().setVisibility(View.INVISIBLE);
+            viewHolder.messageBox().setMinimumHeight(0);
         }
-        viewHolder.contact_picture.setLayoutParams(layoutParams);
+        viewHolder.contactPicture().setLayoutParams(layoutParams);
     }
 
     private boolean mergeIntoTop(final int position, final Message message) {
@@ -1307,7 +1353,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         void onContactPictureLongClicked(View v, Message message);
     }
 
-    private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
+    private static void setBackgroundTint(final LinearLayout view, final BubbleColor bubbleColor) {
         view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
     }
 
@@ -1425,22 +1471,209 @@ 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;
-        protected LinearLayout message_box;
-        protected MaterialButton download_button;
-        protected ImageView image;
-        protected ImageView indicator;
-        protected ImageView indicatorReceived;
-        protected TextView time;
-        protected TextView messageBody;
-        protected ImageView contact_picture;
-        protected TextView status_message;
-        protected TextView encryption;
-        protected ChipGroup reactions;
+    private abstract static class MessageItemViewHolder /*extends RecyclerView.ViewHolder*/ {
+
+        private View itemView;
+
+        private MessageItemViewHolder(@NonNull View itemView) {
+            this.itemView = itemView;
+        }
+    }
+
+    private abstract static class BubbleMessageItemViewHolder extends MessageItemViewHolder {
+
+        private BubbleMessageItemViewHolder(@NonNull View itemView) {
+            super(itemView);
+        }
+
+        public abstract ConstraintLayout root();
+
+        protected abstract ImageView editIndicator();
+
+        protected abstract RelativeLayout audioPlayer();
+
+        protected abstract LinearLayout messageBox();
+
+        protected abstract MaterialButton downloadButton();
+
+        protected abstract ImageView image();
+
+        // TODO rename into indicatorSecurity()
+        protected abstract ImageView indicator();
+
+        protected abstract TextView time();
+
+        protected abstract TextView messageBody();
+
+        protected abstract ImageView contactPicture();
+
+        protected abstract ChipGroup reactions();
+    }
+
+    private static class StartBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder {
+
+        private final ItemMessageReceivedBinding binding;
+
+        public StartBubbleMessageItemViewHolder(@NonNull ItemMessageReceivedBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+
+        @Override
+        public ConstraintLayout root() {
+            return (ConstraintLayout) this.binding.getRoot();
+        }
+
+        @Override
+        protected ImageView editIndicator() {
+            return this.binding.editIndicator;
+        }
+
+        @Override
+        protected RelativeLayout audioPlayer() {
+            return this.binding.messageContent.audioPlayer;
+        }
+
+        @Override
+        protected LinearLayout messageBox() {
+            return this.binding.messageBox;
+        }
+
+        @Override
+        protected MaterialButton downloadButton() {
+            return this.binding.messageContent.downloadButton;
+        }
+
+        @Override
+        protected ImageView image() {
+            return this.binding.messageContent.messageImage;
+        }
+
+        protected ImageView indicator() {
+            return this.binding.securityIndicator;
+        }
+
+        @Override
+        protected TextView time() {
+            return this.binding.messageTime;
+        }
+
+        @Override
+        protected TextView messageBody() {
+            return this.binding.messageContent.messageBody;
+        }
+
+        protected TextView encryption() {
+            return this.binding.messageEncryption;
+        }
+
+        @Override
+        protected ImageView contactPicture() {
+            return this.binding.messagePhoto;
+        }
+
+        @Override
+        protected ChipGroup reactions() {
+            return this.binding.reactions;
+        }
+    }
+
+    private static class EndBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder {
+
+        private final ItemMessageSentBinding binding;
+
+        private EndBubbleMessageItemViewHolder(@NonNull ItemMessageSentBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+
+        @Override
+        public ConstraintLayout root() {
+            return (ConstraintLayout) this.binding.getRoot();
+        }
+
+        @Override
+        protected ImageView editIndicator() {
+            return this.binding.editIndicator;
+        }
+
+        @Override
+        protected RelativeLayout audioPlayer() {
+            return this.binding.messageContent.audioPlayer;
+        }
+
+        @Override
+        protected LinearLayout messageBox() {
+            return this.binding.messageBox;
+        }
+
+        @Override
+        protected MaterialButton downloadButton() {
+            return this.binding.messageContent.downloadButton;
+        }
+
+        @Override
+        protected ImageView image() {
+            return this.binding.messageContent.messageImage;
+        }
+
+        @Override
+        protected ImageView indicator() {
+            return this.binding.securityIndicator;
+        }
+
+        protected ImageView indicatorReceived() {
+            return this.binding.indicatorReceived;
+        }
+
+        @Override
+        protected TextView time() {
+            return this.binding.messageTime;
+        }
+
+        @Override
+        protected TextView messageBody() {
+            return this.binding.messageContent.messageBody;
+        }
+
+        @Override
+        protected ImageView contactPicture() {
+            return this.binding.messagePhoto;
+        }
+
+        @Override
+        protected ChipGroup reactions() {
+            return this.binding.reactions;
+        }
+    }
+
+    private static class DateSeperatorMessageItemViewHolder extends MessageItemViewHolder {
+
+        private final ItemMessageDateBubbleBinding binding;
+
+        private DateSeperatorMessageItemViewHolder(@NonNull ItemMessageDateBubbleBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+    }
+
+    private static class RtpSessionMessageItemViewHolder extends MessageItemViewHolder {
+
+        private final ItemMessageRtpSessionBinding binding;
+
+        private RtpSessionMessageItemViewHolder(@NonNull ItemMessageRtpSessionBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+    }
+
+    private static class StatusMessageItemViewHolder extends MessageItemViewHolder {
+
+        private final ItemMessageStatusBinding binding;
+
+        private StatusMessageItemViewHolder(@NonNull ItemMessageStatusBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
     }
 }