diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java
index bf28e2458a62492f4b05b5fb3bd6e4f85df58fd8..ed82c5adff3ed528d09dc47047decf5f59c5e296 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java
@@ -98,10 +98,11 @@ class AttachmentController {
 	 */
 	List<AttachmentItem> getAttachmentItems(
 			List<Pair<AttachmentHeader, Attachment>> attachments) {
+		boolean needsSize = attachments.size() == 1;
 		List<AttachmentItem> items = new ArrayList<>(attachments.size());
 		for (Pair<AttachmentHeader, Attachment> a : attachments) {
 			AttachmentItem item =
-					getAttachmentItem(a.getFirst(), a.getSecond());
+					getAttachmentItem(a.getFirst(), a.getSecond(), needsSize);
 			items.add(item);
 		}
 		return items;
@@ -111,10 +112,22 @@ class AttachmentController {
 	 * Creates an {@link AttachmentItem} from the {@link Attachment}'s
 	 * {@link InputStream} which will be closed when this method returns.
 	 */
-	AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
+	AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a,
+			boolean needsSize) {
 		MessageId messageId = h.getMessageId();
-		Size size = new Size();
+		if (!needsSize) {
+			String mimeType = h.getContentType();
+			String extension = getExtensionFromMimeType(mimeType);
+			boolean hasError = false;
+			if (extension == null) {
+				extension = "";
+				hasError = true;
+			}
+			return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0,
+					0, hasError);
+		}
 
+		Size size = new Size();
 		InputStream is = new BufferedInputStream(a.getStream());
 		is.mark(Integer.MAX_VALUE);
 		try {
@@ -144,14 +157,19 @@ class AttachmentController {
 					getThumbnailSize(size.width, size.height, size.mimeType);
 		}
 		// get file extension
-		MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
-		String extension = mimeTypeMap.getExtensionFromMimeType(size.mimeType);
+		String extension = getExtensionFromMimeType(size.mimeType);
 		if (extension == null) {
 			return new AttachmentItem(messageId, 0, 0, "", "", 0, 0, true);
 		}
 		return new AttachmentItem(messageId, size.width, size.height,
-				size.mimeType, extension, thumbnailSize.width, thumbnailSize.height,
-				size.error);
+				size.mimeType, extension, thumbnailSize.width,
+				thumbnailSize.height, size.error);
+	}
+
+	@Nullable
+	private String getExtensionFromMimeType(String mimeType) {
+		MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+		return mimeTypeMap.getExtensionFromMimeType(mimeType);
 	}
 
 	/**
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
index 9f48b562838c64fb37b9bf3e4eeaff84c58b563b..f5f6ff37aeac3afabc3bd543407b7107107e80b3 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java
@@ -394,7 +394,7 @@ public class ConversationActivity extends BriarActivity
 								textCache.put(id, text);
 							}
 						}
-						if (!h.getAttachmentHeaders().isEmpty()) {
+						if (h.getAttachmentHeaders().size() == 1) {
 							List<AttachmentItem> items =
 									attachmentController.get(id);
 							if (items == null) {
@@ -483,7 +483,7 @@ public class ConversationActivity extends BriarActivity
 			try {
 				List<Pair<AttachmentHeader, Attachment>> attachments =
 						attachmentController.getMessageAttachments(headers);
-				// TODO move getting the items off to the IoExecutor
+				// TODO move getting the items off to IoExecutor, if size == 1
 				List<AttachmentItem> items =
 						attachmentController.getAttachmentItems(attachments);
 				displayMessageAttachments(messageId, items);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
index e185db3865d08f2a280c2f8405cd3110c4f9de6a..0194a2d7fdc63029d26a4e2fb5d40ac694137466 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
 import android.content.Context;
 import android.support.annotation.LayoutRes;
 import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView.RecycledViewPool;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -21,11 +22,17 @@ class ConversationAdapter
 		extends BriarAdapter<ConversationItem, ConversationItemViewHolder> {
 
 	private ConversationListener listener;
+	private final RecycledViewPool imageViewPool;
+	private final ImageItemDecoration imageItemDecoration;
 
 	ConversationAdapter(Context ctx,
 			ConversationListener conversationListener) {
 		super(ctx, ConversationItem.class);
 		listener = conversationListener;
+		// This shares the same pool for view recycling between all image lists
+		imageViewPool = new RecycledViewPool();
+		// Share the item decoration as well
+		imageItemDecoration = new ImageItemDecoration(ctx);
 	}
 
 	@LayoutRes
@@ -42,15 +49,17 @@ class ConversationAdapter
 				type, viewGroup, false);
 		switch (type) {
 			case R.layout.list_item_conversation_msg_in:
-				return new ConversationMessageViewHolder(v, true);
+				return new ConversationMessageViewHolder(v, listener, true,
+						imageViewPool, imageItemDecoration);
 			case R.layout.list_item_conversation_msg_out:
-				return new ConversationMessageViewHolder(v, false);
+				return new ConversationMessageViewHolder(v, listener, false,
+						imageViewPool, imageItemDecoration);
 			case R.layout.list_item_conversation_notice_in:
-				return new ConversationNoticeViewHolder(v, true);
+				return new ConversationNoticeViewHolder(v, listener, true);
 			case R.layout.list_item_conversation_notice_out:
-				return new ConversationNoticeViewHolder(v, false);
+				return new ConversationNoticeViewHolder(v, listener, false);
 			case R.layout.list_item_conversation_request:
-				return new ConversationRequestViewHolder(v, true);
+				return new ConversationRequestViewHolder(v, listener, true);
 			default:
 				throw new IllegalArgumentException("Unknown ConversationItem");
 		}
@@ -59,7 +68,7 @@ class ConversationAdapter
 	@Override
 	public void onBindViewHolder(ConversationItemViewHolder ui, int position) {
 		ConversationItem item = items.get(position);
-		ui.bind(item, listener);
+		ui.bind(item);
 		listener.onItemVisible(item);
 	}
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
index 7d0e25d949ce1573ea67ba6a987e3d9a535b0636..8ffa2ab4b26d05a7229e7eb5e346274950acc67d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java
@@ -18,14 +18,17 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate;
 @NotNullByDefault
 abstract class ConversationItemViewHolder extends ViewHolder {
 
+	protected final ConversationListener listener;
 	protected final ConstraintLayout layout;
 	@Nullable
 	private final OutItemViewHolder outViewHolder;
 	private final TextView text;
 	protected final TextView time;
 
-	ConversationItemViewHolder(View v, boolean isIncoming) {
+	ConversationItemViewHolder(View v, ConversationListener listener,
+			boolean isIncoming) {
 		super(v);
+		this.listener = listener;
 		this.outViewHolder = isIncoming ? null : new OutItemViewHolder(v);
 		layout = v.findViewById(R.id.layout);
 		text = v.findViewById(R.id.text);
@@ -33,7 +36,7 @@ abstract class ConversationItemViewHolder extends ViewHolder {
 	}
 
 	@CallSuper
-	void bind(ConversationItem item, ConversationListener listener) {
+	void bind(ConversationItem item) {
 		if (item.getText() != null) {
 			text.setText(trim(item.getText()));
 		}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
index abef0bffdb36a4afc6e66a4594cd2550d3a73844..94c80639a0857c3ba3f9d336a06ebb72dae6f544 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java
@@ -1,63 +1,45 @@
 package org.briarproject.briar.android.conversation;
 
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.support.annotation.DrawableRes;
 import android.support.annotation.UiThread;
 import android.support.constraint.ConstraintSet;
-import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.RecycledViewPool;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import com.bumptech.glide.load.Transformation;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.R;
-import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
-import org.briarproject.briar.android.conversation.glide.GlideApp;
 
-import static android.os.Build.VERSION.SDK_INT;
-import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL;
-import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
-import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
+import static android.support.constraint.ConstraintSet.WRAP_CONTENT;
+import static android.support.v4.content.ContextCompat.getColor;
 
 @UiThread
 @NotNullByDefault
 class ConversationMessageViewHolder extends ConversationItemViewHolder {
 
-	@DrawableRes
-	private static final int ERROR_RES = R.drawable.ic_image_broken;
-
-	private final ImageView imageView;
+	private final ImageAdapter adapter;
 	private final ViewGroup statusLayout;
 	private final int timeColor, timeColorBubble;
-	private final int radiusBig, radiusSmall;
-	private final boolean isRtl;
 	private final ConstraintSet textConstraints = new ConstraintSet();
 	private final ConstraintSet imageConstraints = new ConstraintSet();
 	private final ConstraintSet imageTextConstraints = new ConstraintSet();
 
-	ConversationMessageViewHolder(View v, boolean isIncoming) {
-		super(v, isIncoming);
-		imageView = v.findViewById(R.id.imageView);
+	ConversationMessageViewHolder(View v, ConversationListener listener,
+			boolean isIncoming, RecycledViewPool imageViewPool,
+			ImageItemDecoration imageItemDecoration) {
+		super(v, listener, isIncoming);
 		statusLayout = v.findViewById(R.id.statusLayout);
-		radiusBig = v.getContext().getResources()
-				.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
-		radiusSmall = v.getContext().getResources()
-				.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
+
+		// image list
+		RecyclerView list = v.findViewById(R.id.imageList);
+		list.setRecycledViewPool(imageViewPool);
+		adapter = new ImageAdapter(v.getContext(), listener);
+		list.setAdapter(adapter);
+		list.addItemDecoration(imageItemDecoration);
 
 		// remember original status text color
 		timeColor = time.getCurrentTextColor();
-		timeColorBubble =
-				ContextCompat.getColor(v.getContext(), R.color.briar_white);
-
-		// find out if we are showing a RTL language, Use the configuration,
-		// because getting the layout direction of views is not reliable
-		Configuration config =
-				imageView.getContext().getResources().getConfiguration();
-		isRtl = SDK_INT >= 17 &&
-				config.getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+		timeColorBubble = getColor(v.getContext(), R.color.briar_white);
 
 		// clone constraint sets from layout files
 		textConstraints
@@ -77,85 +59,55 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder {
 	}
 
 	@Override
-	void bind(ConversationItem conversationItem,
-			ConversationListener listener) {
-		super.bind(conversationItem, listener);
+	void bind(ConversationItem conversationItem) {
+		super.bind(conversationItem);
 		ConversationMessageItem item =
 				(ConversationMessageItem) conversationItem;
 		if (item.getAttachments().isEmpty()) {
 			bindTextItem();
 		} else {
-			bindImageItem(item, listener);
+			bindImageItem(item);
 		}
 	}
 
 	private void bindTextItem() {
-		clearImage();
-		statusLayout.setBackgroundResource(0);
-		// also reset padding (the background drawable defines some)
-		statusLayout.setPadding(0, 0, 0, 0);
-		time.setTextColor(timeColor);
+		resetStatusLayoutForText();
 		textConstraints.applyTo(layout);
+		adapter.clear();
 	}
 
-	private void bindImageItem(ConversationMessageItem item,
-			ConversationListener listener) {
-		// TODO show more than just the first image
-		AttachmentItem attachment = item.getAttachments().get(0);
-
+	private void bindImageItem(ConversationMessageItem item) {
 		ConstraintSet constraintSet;
 		if (item.getText() == null) {
-			statusLayout
-					.setBackgroundResource(R.drawable.msg_status_bubble);
+			statusLayout.setBackgroundResource(R.drawable.msg_status_bubble);
 			time.setTextColor(timeColorBubble);
 			constraintSet = imageConstraints;
 		} else {
-			statusLayout.setBackgroundResource(0);
-			// also reset padding (the background drawable defines some)
-			statusLayout.setPadding(0, 0, 0, 0);
-			time.setTextColor(timeColor);
+			resetStatusLayoutForText();
 			constraintSet = imageTextConstraints;
 		}
 
-		// apply image size constraints, so glides picks them up for scaling
-		int width = attachment.getThumbnailWidth();
-		int height = attachment.getThumbnailHeight();
-		constraintSet.constrainWidth(R.id.imageView, width);
-		constraintSet.constrainHeight(R.id.imageView, height);
-		constraintSet.applyTo(layout);
-
-		if (attachment.hasError()) {
-			clearImage();
-			imageView.setImageResource(ERROR_RES);
+		if (item.getAttachments().size() == 1) {
+			// apply image size constraints for a single image
+			AttachmentItem attachment = item.getAttachments().get(0);
+			int width = attachment.getThumbnailWidth();
+			int height = attachment.getThumbnailHeight();
+			constraintSet.constrainWidth(R.id.imageList, width);
+			constraintSet.constrainHeight(R.id.imageList, height);
 		} else {
-			loadImage(item, attachment, listener);
+			// bubble adapts to size of image list
+			constraintSet.constrainWidth(R.id.imageList, WRAP_CONTENT);
+			constraintSet.constrainHeight(R.id.imageList, WRAP_CONTENT);
 		}
+		constraintSet.applyTo(layout);
+		adapter.setConversationItem(item);
 	}
 
-	private void clearImage() {
-		GlideApp.with(imageView)
-				.clear(imageView);
-		imageView.setOnClickListener(null);
-	}
-
-	private void loadImage(ConversationMessageItem item,
-			AttachmentItem attachment, ConversationListener listener) {
-		boolean leftCornerSmall =
-				(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
-		boolean bottomRound = item.getText() == null;
-		Transformation<Bitmap> transformation = new BriarImageTransformation(
-				radiusSmall, radiusBig, leftCornerSmall, bottomRound);
-
-		GlideApp.with(imageView)
-				.load(attachment)
-				.diskCacheStrategy(NONE)
-				.error(ERROR_RES)
-				.transform(transformation)
-				.transition(withCrossFade())
-				.into(imageView)
-				.waitForLayout();
-		imageView.setOnClickListener(
-				view -> listener.onAttachmentClicked(view, item, attachment));
+	private void resetStatusLayoutForText() {
+		statusLayout.setBackgroundResource(0);
+		// also reset padding (the background drawable defines some)
+		statusLayout.setPadding(0, 0, 0, 0);
+		time.setTextColor(timeColor);
 	}
 
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java
index 75b58bcca33a3cde7f7239540b2ec64673591ab2..8f34e168be712d794b509300de090b9bdf6c755b 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationNoticeViewHolder.java
@@ -19,16 +19,17 @@ class ConversationNoticeViewHolder extends ConversationItemViewHolder {
 
 	private final TextView msgText;
 
-	ConversationNoticeViewHolder(View v, boolean isIncoming) {
-		super(v, isIncoming);
+	ConversationNoticeViewHolder(View v, ConversationListener listener,
+			boolean isIncoming) {
+		super(v, listener, isIncoming);
 		msgText = v.findViewById(R.id.msgText);
 	}
 
 	@Override
 	@CallSuper
-	void bind(ConversationItem item, ConversationListener listener) {
+	void bind(ConversationItem item) {
 		ConversationNoticeItem notice = (ConversationNoticeItem) item;
-		super.bind(notice, listener);
+		super.bind(notice);
 
 		String text = notice.getMsgText();
 		if (isNullOrEmpty(text)) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java
index 76637131fdbddce5b98064b3ae0f08ebf6ccb2d8..4afaba399aeb6901820f745d978d01ec37b35c84 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationRequestViewHolder.java
@@ -17,16 +17,17 @@ class ConversationRequestViewHolder extends ConversationNoticeViewHolder {
 	private final Button acceptButton;
 	private final Button declineButton;
 
-	ConversationRequestViewHolder(View v, boolean isIncoming) {
-		super(v, isIncoming);
+	ConversationRequestViewHolder(View v, ConversationListener listener,
+			boolean isIncoming) {
+		super(v, listener, isIncoming);
 		acceptButton = v.findViewById(R.id.acceptButton);
 		declineButton = v.findViewById(R.id.declineButton);
 	}
 
 	@Override
-	void bind(ConversationItem item, ConversationListener listener) {
+	void bind(ConversationItem item) {
 		ConversationRequestItem request = (ConversationRequestItem) item;
-		super.bind(request, listener);
+		super.bind(request);
 
 		if (request.wasAnswered() && request.canBeOpened()) {
 			acceptButton.setVisibility(VISIBLE);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
index 7070121572f9472462bc531e8471567ab5ebd271..732c9fb7e32b5eb114d3a55f25c2de2ff6d49e6d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
@@ -160,9 +160,11 @@ public class ConversationViewModel extends AndroidViewModel {
 			long start = now();
 			List<AttachmentHeader> attachments = new ArrayList<>();
 			List<AttachmentItem> items = new ArrayList<>();
+			boolean needsSize = uris.size() == 1;
 			for (Uri uri : uris) {
 				Pair<AttachmentHeader, AttachmentItem> pair =
-						createAttachmentHeader(groupId, uri, timestamp);
+						createAttachmentHeader(groupId, uri, timestamp,
+								needsSize);
 				if (pair == null) continue;
 				attachments.add(pair.getFirst());
 				items.add(pair.getSecond());
@@ -175,7 +177,7 @@ public class ConversationViewModel extends AndroidViewModel {
 	@Nullable
 	@DatabaseExecutor
 	private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
-			GroupId groupId, Uri uri, long timestamp) {
+			GroupId groupId, Uri uri, long timestamp, boolean needsSize) {
 		InputStream is = null;
 		try {
 			ContentResolver contentResolver =
@@ -191,7 +193,7 @@ public class ConversationViewModel extends AndroidViewModel {
 			is = contentResolver.openInputStream(uri);
 			if (is == null) throw new IOException();
 			AttachmentItem item = attachmentController
-					.getAttachmentItem(h, new Attachment(is));
+					.getAttachmentItem(h, new Attachment(is), needsSize);
 			return new Pair<>(h, item);
 		} catch (DbException | IOException e) {
 			logException(LOG, WARNING, e);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c9e15d30ed42eea7ae20666bd894681cba2cf6c
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java
@@ -0,0 +1,155 @@
+package org.briarproject.briar.android.conversation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.conversation.glide.Radii;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.content.Context.WINDOW_SERVICE;
+import static java.util.Objects.requireNonNull;
+import static org.briarproject.briar.android.util.UiUtils.isRtl;
+
+@NotNullByDefault
+class ImageAdapter extends Adapter<ImageViewHolder> {
+
+	private final List<AttachmentItem> items = new ArrayList<>();
+	private final ConversationListener listener;
+	private final int imageSize;
+	private final int radiusBig, radiusSmall;
+	private final boolean isRtl;
+	@Nullable
+	private ConversationMessageItem conversationItem;
+
+	ImageAdapter(Context ctx, ConversationListener listener) {
+		this.listener = listener;
+		imageSize = getImageSize(ctx);
+		Resources res = ctx.getResources();
+		radiusBig =
+				res.getDimensionPixelSize(R.dimen.message_bubble_radius_big);
+		radiusSmall =
+				res.getDimensionPixelSize(R.dimen.message_bubble_radius_small);
+		isRtl = isRtl(ctx);
+	}
+
+	@Override
+	public ImageViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
+		View v = LayoutInflater.from(viewGroup.getContext()).inflate(
+				R.layout.list_item_image, viewGroup, false);
+		return new ImageViewHolder(v, imageSize);
+	}
+
+	@Override
+	public void onBindViewHolder(ImageViewHolder imageViewHolder,
+			int position) {
+		// get item
+		requireNonNull(conversationItem);
+		AttachmentItem item = items.get(position);
+		// set onClick listener
+		imageViewHolder.itemView.setOnClickListener(v ->
+				listener.onAttachmentClicked(v, conversationItem, item)
+		);
+		// bind view holder
+		int size = items.size();
+		boolean isIncoming = conversationItem.isIncoming();
+		boolean hasText = conversationItem.getText() != null;
+		Radii r = getRadii(position, size, isIncoming, hasText);
+		imageViewHolder.bind(item, r, size == 1, singleInRow(position, size));
+	}
+
+	@Override
+	public int getItemCount() {
+		return items.size();
+	}
+
+	void setConversationItem(ConversationMessageItem item) {
+		this.conversationItem = item;
+		this.items.clear();
+		this.items.addAll(item.getAttachments());
+		notifyDataSetChanged();
+	}
+
+	private int getImageSize(Context ctx) {
+		Resources res = ctx.getResources();
+		WindowManager windowManager =
+				(WindowManager) ctx.getSystemService(WINDOW_SERVICE);
+		DisplayMetrics displayMetrics = new DisplayMetrics();
+		if (windowManager == null) {
+			return res.getDimensionPixelSize(
+					R.dimen.message_bubble_image_default);
+		}
+		windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+		int imageSize = displayMetrics.widthPixels / 3;
+		int maxSize = res.getDimensionPixelSize(
+				R.dimen.message_bubble_image_max_width);
+		return Math.min(imageSize, maxSize);
+	}
+
+	private Radii getRadii(int pos, int num, boolean isIncoming,
+			boolean hasText) {
+		boolean left = isLeft(pos);
+		boolean single = num == 1;
+		// Top Row
+		int topLeft;
+		int topRight;
+		if (single) {
+			topLeft = isIncoming ? radiusSmall : radiusBig;
+			topRight = !isIncoming ? radiusSmall : radiusBig;
+		} else if (isTopRow(pos)) {
+			topLeft = left ? (isIncoming ? radiusSmall : radiusBig) : 0;
+			topRight = !left ? (!isIncoming ? radiusSmall : radiusBig) : 0;
+		} else {
+			topLeft = 0;
+			topRight = 0;
+		}
+		// Bottom Row
+		boolean singleInRow = singleInRow(pos, num);
+		int bottomLeft;
+		int bottomRight;
+		if (!hasText && isBottomRow(pos, num)) {
+			bottomLeft = singleInRow || left ? radiusBig : 0;
+			bottomRight = singleInRow || !left ? radiusBig : 0;
+		} else {
+			bottomLeft = 0;
+			bottomRight = 0;
+		}
+		if (isRtl) return new Radii(topRight, topLeft, bottomRight, bottomLeft);
+		return new Radii(topLeft, topRight, bottomLeft, bottomRight);
+	}
+
+	void clear() {
+		items.clear();
+		notifyDataSetChanged();
+	}
+
+	static boolean isTopRow(int pos) {
+		return pos < 2;
+	}
+
+	static boolean isLeft(int pos) {
+		return pos % 2 == 0;
+	}
+
+	static boolean isBottomRow(int pos, int num) {
+		return num % 2 == 0 ?
+				pos >= num - 2 : // last two, if even
+				pos > num - 2;   // last one, if odd
+	}
+
+	static boolean singleInRow(int pos, int num) {
+		// last item of an odd number
+		return num % 2 != 0 && pos == num -1;
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java
new file mode 100644
index 0000000000000000000000000000000000000000..92df42e4037a52537143529df49439401f62ce62
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageItemDecoration.java
@@ -0,0 +1,54 @@
+package org.briarproject.briar.android.conversation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.support.v7.widget.RecyclerView.State;
+import android.view.View;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.util.UiUtils;
+
+import static org.briarproject.briar.android.conversation.ImageAdapter.isBottomRow;
+import static org.briarproject.briar.android.conversation.ImageAdapter.isLeft;
+import static org.briarproject.briar.android.conversation.ImageAdapter.isTopRow;
+import static org.briarproject.briar.android.conversation.ImageAdapter.singleInRow;
+
+@NotNullByDefault
+class ImageItemDecoration extends ItemDecoration {
+
+	private final int border;
+	private final boolean isRtl;
+
+	ImageItemDecoration(Context ctx) {
+		Resources res = ctx.getResources();
+
+		// for pixel perfection, add a pixel to the border if it has an odd size
+		int b = res.getDimensionPixelSize(R.dimen.message_bubble_border);
+		int realBorderSize = b % 2 == 0 ? b : b + 1;
+
+		// we are applying half the border around the insides of each image
+		// to prevent differently sized images looking slightly broken
+		border = realBorderSize / 2;
+
+		// find out if we are showing a RTL language
+		isRtl = UiUtils.isRtl(ctx);
+	}
+
+	@Override
+	public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+			State state) {
+		if (state.getItemCount() == 1) return;
+		int pos = parent.getChildAdapterPosition(view);
+		int num = state.getItemCount();
+		boolean start = isLeft(pos) ^ isRtl;
+		outRect.top = isTopRow(pos) ? 0 : border;
+		outRect.left = start ? 0 : border;
+		outRect.right = start && !singleInRow(pos, num) ? border : 0;
+		outRect.bottom = isBottomRow(pos, num) ? 0 : border;
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..301fa3c13f4550ad4466fd4865ac74e8445d073f
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java
@@ -0,0 +1,70 @@
+package org.briarproject.briar.android.conversation;
+
+import android.graphics.Bitmap;
+import android.support.annotation.DrawableRes;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.bumptech.glide.load.Transformation;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.R;
+import org.briarproject.briar.android.conversation.glide.BriarImageTransformation;
+import org.briarproject.briar.android.conversation.glide.GlideApp;
+import org.briarproject.briar.android.conversation.glide.Radii;
+
+import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
+import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
+
+@NotNullByDefault
+class ImageViewHolder extends ViewHolder {
+
+	@DrawableRes
+	private static final int ERROR_RES = R.drawable.ic_image_broken;
+
+	protected final ImageView imageView;
+	private final int imageSize;
+
+	ImageViewHolder(View v, int imageSize) {
+		super(v);
+		imageView = v.findViewById(R.id.imageView);
+		this.imageSize = imageSize;
+	}
+
+	void bind(AttachmentItem attachment, Radii r, boolean single,
+			boolean needsStretch) {
+		if (attachment.hasError()) {
+			GlideApp.with(imageView)
+					.clear(imageView);
+			imageView.setImageResource(ERROR_RES);
+		} else {
+			setImageViewDimensions(attachment, single, needsStretch);
+			loadImage(attachment, r);
+		}
+	}
+
+	private void setImageViewDimensions(AttachmentItem a, boolean single,
+			boolean needsStretch) {
+		LayoutParams params = (LayoutParams) imageView.getLayoutParams();
+		int width = needsStretch ? imageSize * 2 : imageSize;
+		params.width = single ? a.getThumbnailWidth() : width;
+		params.height = single ? a.getThumbnailHeight() : imageSize;
+		params.setFullSpan(!single && needsStretch);
+		imageView.setLayoutParams(params);
+	}
+
+	private void loadImage(AttachmentItem a, Radii r) {
+		Transformation<Bitmap> transformation = new BriarImageTransformation(r);
+		GlideApp.with(imageView)
+				.load(a)
+				.diskCacheStrategy(NONE)
+				.error(ERROR_RES)
+				.transform(transformation)
+				.transition(withCrossFade())
+				.into(imageView)
+				.waitForLayout();
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java
index 694887c911460e4decc98b5908c5fdd8514c52e4..d3cc620196f439edd1dfb3e576902440b8d0f5c5 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java
@@ -48,10 +48,10 @@ public class ImageViewModel extends AndroidViewModel {
 	@IoExecutor
 	private final Executor ioExecutor;
 
-	private MutableLiveData<Boolean> saveState = new MutableLiveData<>();
+	private final MutableLiveData<Boolean> saveState = new MutableLiveData<>();
 
 	@Inject
-	public ImageViewModel(Application application,
+	ImageViewModel(Application application,
 			MessagingManager messagingManager,
 			@DatabaseExecutor Executor dbExecutor,
 			@IoExecutor Executor ioExecutor) {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java
index 5488efa402e09a0705d6f74243a5a75c1c6aac2c..c2bb1dbc0d8cbb2fd00d99dd611f1af4bd85d997 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java
@@ -7,10 +7,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
 
 public class BriarImageTransformation extends MultiTransformation<Bitmap> {
 
-	public BriarImageTransformation(int smallRadius, int radius,
-			boolean leftCornerSmall, boolean bottomRound) {
-		super(new CenterCrop(), new ImageCornerTransformation(
-				smallRadius, radius, leftCornerSmall, bottomRound));
+	public BriarImageTransformation(Radii r) {
+		super(new CenterCrop(), new CustomCornersTransformation(r));
 	}
 
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc013750c0ccf202ed22297ec14ed360881791e0
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/CustomCornersTransformation.java
@@ -0,0 +1,128 @@
+package org.briarproject.briar.android.conversation.glide;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.support.annotation.NonNull;
+
+import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
+import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.security.MessageDigest;
+
+import javax.annotation.concurrent.Immutable;
+
+import static android.graphics.Bitmap.Config.ARGB_8888;
+import static android.graphics.Shader.TileMode.CLAMP;
+
+@Immutable
+@NotNullByDefault
+public class CustomCornersTransformation extends BitmapTransformation {
+
+	private static final String ID = CustomCornersTransformation.class.getName();
+
+	private final Radii radii;
+
+	public CustomCornersTransformation(Radii radii) {
+		this.radii = radii;
+	}
+
+	@Override
+	protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
+			int outWidth, int outHeight) {
+		int width = toTransform.getWidth();
+		int height = toTransform.getHeight();
+
+		Bitmap bitmap = pool.get(width, height, ARGB_8888);
+		bitmap.setHasAlpha(true);
+
+		Canvas canvas = new Canvas(bitmap);
+		Paint paint = new Paint();
+		paint.setAntiAlias(true);
+		paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
+		drawRect(canvas, paint, width, height);
+		return bitmap;
+	}
+
+	private void drawRect(Canvas canvas, Paint paint, float width,
+			float height) {
+		drawTopLeft(canvas, paint, radii.topLeft, width, height);
+		drawTopRight(canvas, paint, radii.topRight, width, height);
+		drawBottomLeft(canvas, paint, radii.bottomLeft, width, height);
+		drawBottomRight(canvas, paint, radii.bottomRight, width, height);
+	}
+
+	private void drawTopLeft(Canvas canvas, Paint paint, int radius,
+			float width, float height) {
+		RectF rect = new RectF(
+				0,
+				0,
+				width / 2 + radius + 1,
+				height / 2 + radius + 1
+		);
+		if (radius == 0) canvas.drawRect(rect, paint);
+		else canvas.drawRoundRect(rect, radius, radius, paint);
+	}
+
+	private void drawTopRight(Canvas canvas, Paint paint, int radius,
+			float width, float height) {
+		RectF rect = new RectF(
+				width / 2 - radius,
+				0,
+				width,
+				height / 2 + radius + 1
+		);
+		if (radius == 0) canvas.drawRect(rect, paint);
+		else canvas.drawRoundRect(rect, radius, radius, paint);
+	}
+
+	private void drawBottomLeft(Canvas canvas, Paint paint, int radius,
+			float width, float height) {
+		RectF rect = new RectF(
+				0,
+				height / 2 - radius,
+				width / 2 + radius + 1,
+				height
+		);
+		if (radius == 0) canvas.drawRect(rect, paint);
+		else canvas.drawRoundRect(rect, radius, radius, paint);
+	}
+
+	private void drawBottomRight(Canvas canvas, Paint paint, int radius,
+			float width, float height) {
+		RectF rect = new RectF(
+				width / 2 - radius,
+				height / 2 - radius,
+				width,
+				height
+		);
+		if (radius == 0) canvas.drawRect(rect, paint);
+		else canvas.drawRoundRect(rect, radius, radius, paint);
+	}
+
+	@Override
+	public String toString() {
+		return "ImageCornerTransformation(" + radii + ")";
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return o instanceof CustomCornersTransformation &&
+				radii.equals(((CustomCornersTransformation) o).radii);
+	}
+
+	@Override
+	public int hashCode() {
+		return ID.hashCode() + radii.hashCode();
+	}
+
+	@Override
+	public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
+		messageDigest.update((ID + radii).getBytes(CHARSET));
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java
deleted file mode 100644
index b41c0cf3177b253036d689fd1c4baafc33340df9..0000000000000000000000000000000000000000
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.briarproject.briar.android.conversation.glide;
-
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.support.annotation.NonNull;
-
-import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
-import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-import java.security.MessageDigest;
-
-import javax.annotation.concurrent.Immutable;
-
-import static android.graphics.Bitmap.Config.ARGB_8888;
-import static android.graphics.Shader.TileMode.CLAMP;
-
-@Immutable
-@NotNullByDefault
-class ImageCornerTransformation extends BitmapTransformation {
-
-	private static final String ID = ImageCornerTransformation.class.getName();
-
-	private final int smallRadius, radius;
-	private final boolean leftCornerSmall, bottomRound;
-
-	ImageCornerTransformation(int smallRadius, int radius,
-			boolean leftCornerSmall, boolean bottomRound) {
-		this.smallRadius = smallRadius;
-		this.radius = radius;
-		this.leftCornerSmall = leftCornerSmall;
-		this.bottomRound = bottomRound;
-	}
-
-	@Override
-	protected Bitmap transform(BitmapPool pool, Bitmap toTransform,
-			int outWidth, int outHeight) {
-		int width = toTransform.getWidth();
-		int height = toTransform.getHeight();
-
-		Bitmap bitmap = pool.get(width, height, ARGB_8888);
-		bitmap.setHasAlpha(true);
-
-		Canvas canvas = new Canvas(bitmap);
-		Paint paint = new Paint();
-		paint.setAntiAlias(true);
-		paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP));
-		drawRect(canvas, paint, width, height);
-		return bitmap;
-	}
-
-	private void drawRect(Canvas canvas, Paint paint, float width,
-			float height) {
-		drawSmallCorner(canvas, paint, width);
-		drawBigCorners(canvas, paint, width, height);
-	}
-
-	private void drawSmallCorner(Canvas canvas, Paint paint, float width) {
-		float left = leftCornerSmall ? 0 : width - radius;
-		float right = leftCornerSmall ? radius : width;
-		canvas.drawRoundRect(new RectF(left, 0, right, radius),
-				smallRadius, smallRadius, paint);
-	}
-
-	private void drawBigCorners(Canvas canvas, Paint paint, float width,
-			float height) {
-		float top = bottomRound ? 0 : radius;
-		RectF rect = new RectF(0, top, width, height);
-		if (bottomRound) {
-			canvas.drawRoundRect(rect, radius, radius, paint);
-		} else {
-			canvas.drawRect(rect, paint);
-			canvas.drawRoundRect(new RectF(0, 0, width, radius * 2),
-					radius, radius, paint);
-		}
-	}
-
-	@Override
-	public String toString() {
-		return "ImageCornerTransformation(smallRadius=" + smallRadius +
-				", radius=" + radius + ", leftCornerSmall=" + leftCornerSmall +
-				", bottomRound=" + bottomRound + ")";
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		return o instanceof ImageCornerTransformation &&
-				((ImageCornerTransformation) o).smallRadius == smallRadius &&
-				((ImageCornerTransformation) o).radius == radius &&
-				((ImageCornerTransformation) o).leftCornerSmall ==
-						leftCornerSmall &&
-				((ImageCornerTransformation) o).bottomRound == bottomRound;
-	}
-
-	@Override
-	public int hashCode() {
-		return ID.hashCode() + (smallRadius << 16) ^ (radius << 2) ^
-				(leftCornerSmall ? 2 : 0) ^ (bottomRound ? 1 : 0);
-	}
-
-	@Override
-	public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
-		messageDigest.update((ID + '|' + smallRadius + '|' + radius + '|' +
-				leftCornerSmall + '|' + bottomRound).getBytes(CHARSET));
-	}
-
-}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java
new file mode 100644
index 0000000000000000000000000000000000000000..318140ff0149422a26bdaab01c1fdf9abf343f26
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/Radii.java
@@ -0,0 +1,41 @@
+package org.briarproject.briar.android.conversation.glide;
+
+import android.support.annotation.Nullable;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+public class Radii {
+
+	public final int topLeft, topRight, bottomLeft, bottomRight;
+
+	public Radii(int topLeft, int topRight, int bottomLeft, int bottomRight) {
+		this.topLeft = topLeft;
+		this.topRight = topRight;
+		this.bottomLeft = bottomLeft;
+		this.bottomRight = bottomRight;
+	}
+
+	@Override
+	public boolean equals(@Nullable Object o) {
+		return o instanceof Radii &&
+				topLeft == ((Radii) o).topLeft &&
+				topRight == ((Radii) o).topRight &&
+				bottomLeft == ((Radii) o).bottomLeft &&
+				bottomRight == ((Radii) o).bottomRight;
+	}
+
+	@Override
+	public int hashCode() {
+		return topLeft << 24 ^ topRight << 16 ^ bottomLeft << 8 ^ bottomRight;
+	}
+
+	@Override
+	public String toString() {
+		return "Radii(topLeft=" + topLeft +
+				",topRight=" + topRight +
+				",bottomLeft=" + bottomLeft +
+				",bottomRight=" + bottomRight;
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
index b23f24dcde92b8fddef180d17d1cf91220ad20d2..b57a1c9c181b7d88142446cbb7d12fc81a70c51b 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java
@@ -56,6 +56,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static android.os.Build.MANUFACTURER;
 import static android.os.Build.VERSION.SDK_INT;
 import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS;
+import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL;
 import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_AUTO;
 import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;
 import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_NO;
@@ -372,4 +373,10 @@ public class UiUtils {
 		});
 	}
 
+	public static boolean isRtl(Context ctx) {
+		if (SDK_INT < 17) return false;
+		return ctx.getResources().getConfiguration().getLayoutDirection() ==
+				LAYOUT_DIRECTION_RTL;
+	}
+
 }
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
index c36a3080c5c3b8e004ffd5566dda27d74269e4bc..c7459d36fc7353d74fa21691a4814526030dca13 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml
@@ -15,15 +15,19 @@
 	android:background="@drawable/msg_in"
 	android:elevation="@dimen/message_bubble_elevation">
 
-	<ImageView
-		android:id="@+id/imageView"
+	<android.support.v7.widget.RecyclerView
+		android:id="@+id/imageList"
 		android:layout_width="@dimen/message_bubble_image_default"
 		android:layout_height="@dimen/message_bubble_image_default"
+		android:orientation="vertical"
+		app:layoutManager="android.support.v7.widget.StaggeredGridLayoutManager"
 		app:layout_constraintBottom_toTopOf="@+id/text"
+		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="parent"
+		app:spanCount="2"
 		tools:ignore="ContentDescription"
-		tools:src="@drawable/alerts_and_states_error"/>
+		tools:listitem="@layout/list_item_image"/>
 
 	<com.vanniktech.emoji.EmojiTextView
 		android:id="@+id/text"
@@ -38,10 +42,10 @@
 		android:visibility="gone"
 		app:layout_constrainedWidth="true"
 		app:layout_constraintBottom_toTopOf="@+id/statusLayout"
-		app:layout_constraintEnd_toEndOf="@+id/imageView"
+		app:layout_constraintEnd_toEndOf="@+id/imageList"
 		app:layout_constraintHorizontal_bias="0.0"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/imageView"
+		app:layout_constraintTop_toBottomOf="@+id/imageList"
 		tools:text="The text of a message which can sometimes be a bit longer as well"/>
 
 	<LinearLayout
@@ -53,7 +57,7 @@
 		android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
 		android:background="@drawable/msg_status_bubble"
 		android:orientation="horizontal"
-		app:layout_constraintBottom_toBottomOf="@+id/imageView"
+		app:layout_constraintBottom_toBottomOf="@+id/imageList"
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintHorizontal_bias="1.0"
 		app:layout_constraintStart_toStartOf="parent">
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
index 5507d041da0fa1861548d7e3a4f9a799af55d7e6..e90bd02211dcc6ef4f37bf0ee09b2d7cbcebbeea 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml
@@ -15,15 +15,19 @@
 	android:background="@drawable/msg_in"
 	android:elevation="@dimen/message_bubble_elevation">
 
-	<ImageView
-		android:id="@+id/imageView"
+	<android.support.v7.widget.RecyclerView
+		android:id="@+id/imageList"
 		android:layout_width="@dimen/message_bubble_image_default"
 		android:layout_height="@dimen/message_bubble_image_default"
+		android:orientation="vertical"
+		app:layoutManager="android.support.v7.widget.StaggeredGridLayoutManager"
 		app:layout_constraintBottom_toTopOf="@+id/text"
+		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="parent"
+		app:spanCount="2"
 		tools:ignore="ContentDescription"
-		tools:src="@drawable/alerts_and_states_error"/>
+		tools:listitem="@layout/list_item_image"/>
 
 	<com.vanniktech.emoji.EmojiTextView
 		android:id="@+id/text"
@@ -37,10 +41,10 @@
 		android:textColor="?android:attr/textColorPrimary"
 		app:layout_constrainedWidth="true"
 		app:layout_constraintBottom_toTopOf="@+id/statusLayout"
-		app:layout_constraintEnd_toEndOf="@+id/imageView"
+		app:layout_constraintEnd_toEndOf="@+id/imageList"
 		app:layout_constraintHorizontal_bias="0.0"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/imageView"
+		app:layout_constraintTop_toBottomOf="@+id/imageList"
 		tools:text="The text of a message which can sometimes be a bit longer as well"/>
 
 	<LinearLayout
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
index 4e3950a870172fdea5cffa277e507915e83f0dc3..17f3937085187325fe737537b8864575b0bc0fcf 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml
@@ -15,15 +15,20 @@
 	android:background="@drawable/msg_in"
 	android:elevation="@dimen/message_bubble_elevation">
 
-	<ImageView
-		android:id="@+id/imageView"
+	<android.support.v7.widget.RecyclerView
+		android:id="@+id/imageList"
 		android:layout_width="@dimen/message_bubble_image_default"
 		android:layout_height="@dimen/message_bubble_image_default"
+		android:orientation="vertical"
 		android:visibility="gone"
+		app:layoutManager="android.support.v7.widget.StaggeredGridLayoutManager"
 		app:layout_constraintBottom_toTopOf="@+id/text"
+		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintStart_toStartOf="parent"
 		app:layout_constraintTop_toTopOf="parent"
-		tools:ignore="ContentDescription"/>
+		app:spanCount="2"
+		tools:ignore="ContentDescription"
+		tools:listitem="@layout/list_item_image"/>
 
 	<com.vanniktech.emoji.EmojiTextView
 		android:id="@+id/text"
@@ -40,7 +45,7 @@
 		app:layout_constraintEnd_toEndOf="parent"
 		app:layout_constraintHorizontal_bias="0.0"
 		app:layout_constraintStart_toStartOf="parent"
-		app:layout_constraintTop_toBottomOf="@+id/imageView"
+		app:layout_constraintTop_toBottomOf="@+id/imageList"
 		tools:text="The text of a message which can sometimes be a bit longer as well"/>
 
 	<LinearLayout
diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
index d0dc1736e0e00186dab8ce63aafdbb7edce38934..71fcde591a3a26305ecaa186da3bbfc1856e2fbe 100644
--- a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
+++ b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml
@@ -21,14 +21,18 @@
 		android:background="@drawable/msg_out"
 		android:elevation="@dimen/message_bubble_elevation">
 
-		<ImageView
-			android:id="@+id/imageView"
+		<android.support.v7.widget.RecyclerView
+			android:id="@+id/imageList"
 			android:layout_width="@dimen/message_bubble_image_default"
 			android:layout_height="@dimen/message_bubble_image_default"
+			android:orientation="vertical"
 			android:visibility="gone"
+			app:layoutManager="android.support.v7.widget.StaggeredGridLayoutManager"
 			app:layout_constraintBottom_toTopOf="@+id/text"
+			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintStart_toStartOf="parent"
 			app:layout_constraintTop_toTopOf="parent"
+			app:spanCount="2"
 			tools:ignore="ContentDescription"
 			tools:src="@drawable/alerts_and_states_error"/>
 
@@ -47,7 +51,7 @@
 			app:layout_constraintEnd_toEndOf="parent"
 			app:layout_constraintHorizontal_bias="0.0"
 			app:layout_constraintStart_toStartOf="parent"
-			app:layout_constraintTop_toBottomOf="@+id/imageView"
+			app:layout_constraintTop_toBottomOf="@+id/imageList"
 			tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
 
 		<LinearLayout
diff --git a/briar-android/src/main/res/layout/list_item_image.xml b/briar-android/src/main/res/layout/list_item_image.xml
new file mode 100644
index 0000000000000000000000000000000000000000..275d9df1d1587bc4273de3fe3107fe7fe16a72b8
--- /dev/null
+++ b/briar-android/src/main/res/layout/list_item_image.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView
+	android:id="@+id/imageView"
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="@dimen/message_bubble_image_default"
+	android:layout_height="@dimen/message_bubble_image_default"
+	android:scaleType="centerCrop"
+	tools:ignore="ContentDescription"
+	tools:srcCompat="@tools:sample/avatars"/>
diff --git a/briar-android/src/main/res/values/dimens.xml b/briar-android/src/main/res/values/dimens.xml
index 81f7d4377f355872cad298ab03b76e25fc908541..8828a0799449e809127a0ea4d4f78aa21a9e8252 100644
--- a/briar-android/src/main/res/values/dimens.xml
+++ b/briar-android/src/main/res/values/dimens.xml
@@ -43,7 +43,7 @@
 	<dimen name="message_bubble_radius_top_inner">@dimen/message_bubble_radius_small</dimen>
 	<dimen name="message_bubble_radius_top_outer">@dimen/message_bubble_radius_big</dimen>
 	<dimen name="message_bubble_margin">6dp</dimen>
-	<dimen name="message_bubble_image_default">210dp</dimen>
+	<dimen name="message_bubble_image_default">115dp</dimen>
 	<dimen name="message_bubble_image_min_width">150dp</dimen>
 	<dimen name="message_bubble_image_max_width">240dp</dimen>
 	<dimen name="message_bubble_image_min_height">100dp</dimen>