From 0491c3cace968f0a3622a8901e190ad25eff058e Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Thu, 20 Sep 2018 16:11:35 +0100
Subject: [PATCH] Use a visitor to create ConversationItems.

---
 .../android/contact/ContactListFragment.java  |   7 +-
 .../android/contact/ContactListItem.java      |   8 +-
 .../android/contact/ConversationActivity.java |  43 ++-
 .../android/contact/ConversationItem.java     |  48 ----
 .../contact/ConversationNoticeInItem.java     |  68 +----
 .../contact/ConversationNoticeOutItem.java    |  77 +-----
 .../contact/ConversationRequestItem.java      |  64 +----
 .../android/contact/ConversationVisitor.java  | 246 ++++++++++++++++++
 8 files changed, 286 insertions(+), 275 deletions(-)
 create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationVisitor.java

diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
index a3ca713b8e..fefbdeb61d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListFragment.java
@@ -1,7 +1,6 @@
 package org.briarproject.briar.android.contact;
 
 import android.content.Intent;
-import android.os.Build;
 import android.os.Bundle;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.ActivityOptionsCompat;
@@ -48,6 +47,7 @@ import java.util.logging.Logger;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
+import static android.os.Build.VERSION.SDK_INT;
 import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
 import static android.support.v4.view.ViewCompat.getTransitionName;
 import static java.util.logging.Level.WARNING;
@@ -113,7 +113,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 					ContactId contactId = item.getContact().getId();
 					i.putExtra(CONTACT_ID, contactId.getInt());
 
-					if (Build.VERSION.SDK_INT >= 23) {
+					if (SDK_INT >= 23) {
 						ContactListItemViewHolder holder =
 								(ContactListItemViewHolder) list
 										.getRecyclerView()
@@ -256,8 +256,7 @@ public class ContactListFragment extends BaseFragment implements EventListener {
 			int position = adapter.findItemPosition(c);
 			ContactListItem item = adapter.getItemAt(position);
 			if (item != null) {
-				ConversationItem i = ConversationItem.from(getContext(), "", h);
-				item.addMessage(i);
+				item.addMessage(h);
 				adapter.updateItemAt(position, item);
 			}
 		});
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListItem.java
index c8e47207c9..7e910358cc 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ContactListItem.java
@@ -3,6 +3,7 @@ package org.briarproject.briar.android.contact;
 import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.client.MessageTracker.GroupCount;
+import org.briarproject.briar.api.messaging.PrivateMessageHeader;
 
 import javax.annotation.concurrent.NotThreadSafe;
 
@@ -22,11 +23,10 @@ public class ContactListItem extends ContactItem {
 		this.timestamp = count.getLatestMsgTime();
 	}
 
-	void addMessage(ConversationItem message) {
+	void addMessage(PrivateMessageHeader h) {
 		empty = false;
-		if (message.getTime() > timestamp) timestamp = message.getTime();
-		if (!message.isRead())
-			unread++;
+		if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp();
+		if (!h.isRead()) unread++;
 	}
 
 	boolean isEmpty() {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
index 381cd5f921..6e6ce6d785 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java
@@ -53,6 +53,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
 import org.briarproject.briar.android.activity.BriarActivity;
 import org.briarproject.briar.android.blog.BlogActivity;
 import org.briarproject.briar.android.contact.ConversationAdapter.ConversationListener;
+import org.briarproject.briar.android.contact.ConversationVisitor.BodyCache;
 import org.briarproject.briar.android.forum.ForumActivity;
 import org.briarproject.briar.android.introduction.IntroductionActivity;
 import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
@@ -111,7 +112,8 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
 public class ConversationActivity extends BriarActivity
-		implements EventListener, ConversationListener, TextInputListener {
+		implements EventListener, ConversationListener, TextInputListener,
+		BodyCache {
 
 	public static final String CONTACT_ID = "briar.CONTACT_ID";
 
@@ -131,6 +133,7 @@ public class ConversationActivity extends BriarActivity
 	private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
 	private final MutableLiveData<String> contactName = new MutableLiveData<>();
 
+	private ConversationVisitor visitor;
 	private ConversationAdapter adapter;
 	private Toolbar toolbar;
 	private CircleImageView toolbarAvatar;
@@ -191,6 +194,7 @@ public class ConversationActivity extends BriarActivity
 		setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
 		setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
 
+		visitor = new ConversationVisitor(this, this, contactName);
 		adapter = new ConversationAdapter(this, this);
 		list = findViewById(R.id.conversationView);
 		list.setLayoutManager(new LinearLayoutManager(this));
@@ -362,18 +366,7 @@ public class ConversationActivity extends BriarActivity
 	private List<ConversationItem> createItems(
 			Collection<PrivateMessageHeader> headers) {
 		List<ConversationItem> items = new ArrayList<>(headers.size());
-		for (PrivateMessageHeader h : headers) {
-			ConversationItem item;
-			if (h instanceof PrivateRequest || h instanceof PrivateResponse) {
-				item = ConversationItem.from(this, contactName.getValue(), h);
-			} else {
-				item = ConversationItem.from(h);
-				String body = bodyCache.get(h.getId());
-				if (body == null) loadMessageBody(h.getId());
-				else item.setBody(body);
-			}
-			items.add(item);
-		}
+		for (PrivateMessageHeader h : headers) items.add(h.accept(visitor));
 		return items;
 	}
 
@@ -467,27 +460,21 @@ public class ConversationActivity extends BriarActivity
 						@Override
 						public void onChanged(@Nullable String cName) {
 							if (cName != null) {
-								onNewPrivateRequestOrResponse(h, cName);
+								addConversationItem(h.accept(visitor));
 								contactName.removeObserver(this);
 							}
 						}
 					});
 				} else {
-					onNewPrivateRequestOrResponse(h, cName);
+					addConversationItem(h.accept(visitor));
 				}
 			} else {
-				addConversationItem(ConversationItem.from(h));
+				addConversationItem(h.accept(visitor));
 				loadMessageBody(h.getId());
 			}
 		});
 	}
 
-	@UiThread
-	private void onNewPrivateRequestOrResponse(PrivateMessageHeader h,
-			String cName) {
-		addConversationItem(ConversationItem.from(this, cName, h));
-	}
-
 	private void markMessages(Collection<MessageId> messageIds, boolean sent,
 			boolean seen) {
 		runOnUiThreadUnlessDestroyed(() -> {
@@ -558,10 +545,8 @@ public class ConversationActivity extends BriarActivity
 				PrivateMessageHeader h = new PrivateMessageHeader(
 						message.getId(), message.getGroupId(),
 						message.getTimestamp(), true, false, false, false);
-				ConversationItem item = ConversationItem.from(h);
-				item.setBody(body);
 				bodyCache.put(message.getId(), body);
-				addConversationItem(item);
+				addConversationItem(h.accept(visitor));
 			} catch (DbException e) {
 				logException(LOG, WARNING, e);
 			}
@@ -773,4 +758,12 @@ public class ConversationActivity extends BriarActivity
 			throws DbException {
 		groupInvitationManager.respondToInvitation(contactId, id, accept);
 	}
+
+	@Nullable
+	@Override
+	public String getBody(MessageId m) {
+		String body = bodyCache.get(m);
+		if (body == null) loadMessageBody(m);
+		return body;
+	}
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
index 671f7c6d25..98b72b3900 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationItem.java
@@ -1,14 +1,10 @@
 package org.briarproject.briar.android.contact;
 
-import android.content.Context;
 import android.support.annotation.LayoutRes;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.api.messaging.PrivateMessageHeader;
-import org.briarproject.briar.api.messaging.PrivateRequest;
-import org.briarproject.briar.api.messaging.PrivateResponse;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
@@ -62,48 +58,4 @@ abstract class ConversationItem {
 
 	@LayoutRes
 	abstract public int getLayout();
-
-	static ConversationItem from(PrivateMessageHeader h) {
-		if (h.isLocal()) {
-			return new ConversationMessageOutItem(h);
-		} else {
-			return new ConversationMessageInItem(h);
-		}
-	}
-
-	static ConversationItem from(Context ctx, String contactName,
-			PrivateMessageHeader h) {
-		if (h.isLocal()) {
-			return fromLocal(ctx, contactName, h);
-		} else {
-			return fromRemote(ctx, contactName, h);
-		}
-	}
-
-	private static ConversationItem fromLocal(Context ctx, String contactName,
-			PrivateMessageHeader h) {
-		if (h instanceof PrivateRequest) {
-			PrivateRequest r = (PrivateRequest) h;
-			return new ConversationNoticeOutItem(ctx, contactName, r);
-		} else if (h instanceof PrivateResponse) {
-			PrivateResponse r = (PrivateResponse) h;
-			return new ConversationNoticeOutItem(ctx, contactName, r);
-		} else {
-			return new ConversationMessageOutItem(h);
-		}
-	}
-
-	private static ConversationItem fromRemote(Context ctx, String contactName,
-			PrivateMessageHeader h) {
-		if (h instanceof PrivateRequest) {
-			PrivateRequest r = (PrivateRequest) h;
-			return new ConversationRequestItem(ctx, contactName, r);
-		} else if (h instanceof PrivateResponse) {
-			PrivateResponse r = (PrivateResponse) h;
-			return new ConversationNoticeInItem(ctx, contactName, r);
-		} else {
-			return new ConversationMessageInItem(h);
-		}
-	}
-
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeInItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeInItem.java
index 2873ad64c9..c28a97299d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeInItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeInItem.java
@@ -1,18 +1,12 @@
 package org.briarproject.briar.android.contact;
 
-import android.content.Context;
 import android.support.annotation.LayoutRes;
-import android.support.annotation.StringRes;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.briar.R;
-import org.briarproject.briar.api.blog.BlogInvitationResponse;
-import org.briarproject.briar.api.forum.ForumInvitationResponse;
-import org.briarproject.briar.api.introduction.IntroductionResponse;
 import org.briarproject.briar.api.messaging.PrivateResponse;
-import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
@@ -24,17 +18,14 @@ class ConversationNoticeInItem extends ConversationItem {
 	@Nullable
 	private final String msgText;
 
-	ConversationNoticeInItem(MessageId id, GroupId groupId,
-			String text, @Nullable String msgText, long time,
-			boolean read) {
+	ConversationNoticeInItem(MessageId id, GroupId groupId, String text,
+			@Nullable String msgText, long time, boolean read) {
 		super(id, groupId, text, time, read);
 		this.msgText = msgText;
 	}
 
-	public ConversationNoticeInItem(Context ctx, String contactName,
-			PrivateResponse r) {
-		super(r.getId(), r.getGroupId(), getText(ctx, contactName, r),
-				r.getTimestamp(), r.isRead());
+	ConversationNoticeInItem(String text, PrivateResponse r) {
+		super(r.getId(), r.getGroupId(), text, r.getTimestamp(), r.isRead());
 		this.msgText = null;
 	}
 
@@ -53,55 +44,4 @@ class ConversationNoticeInItem extends ConversationItem {
 	public int getLayout() {
 		return R.layout.list_item_conversation_notice_in;
 	}
-
-	private static String getText(Context ctx, String contactName,
-			PrivateResponse r) {
-		if (r.wasAccepted()) {
-			if (r instanceof IntroductionResponse) {
-				IntroductionResponse ir = (IntroductionResponse) r;
-				return ctx.getString(
-						R.string.introduction_response_accepted_received,
-						contactName, ir.getIntroducedAuthor().getName());
-			} else if (r instanceof ForumInvitationResponse) {
-				return ctx.getString(
-						R.string.forum_invitation_response_accepted_received,
-						contactName);
-			} else if (r instanceof BlogInvitationResponse) {
-				return ctx.getString(
-						R.string.blogs_sharing_response_accepted_received,
-						contactName);
-			} else if (r instanceof GroupInvitationResponse) {
-				return ctx.getString(
-						R.string.groups_invitations_response_accepted_received,
-						contactName);
-			}
-		} else {
-			if (r instanceof IntroductionResponse) {
-				IntroductionResponse ir = (IntroductionResponse) r;
-				@StringRes int res;
-				if (ir.isIntroducer()) {
-					res = R.string.introduction_response_declined_received;
-				} else {
-					res =
-							R.string.introduction_response_declined_received_by_introducee;
-				}
-				return ctx.getString(res, contactName,
-						ir.getIntroducedAuthor().getName());
-			} else if (r instanceof ForumInvitationResponse) {
-				return ctx.getString(
-						R.string.forum_invitation_response_declined_received,
-						contactName);
-			} else if (r instanceof BlogInvitationResponse) {
-				return ctx.getString(
-						R.string.blogs_sharing_response_declined_received,
-						contactName);
-			} else if (r instanceof GroupInvitationResponse) {
-				return ctx.getString(
-						R.string.groups_invitations_response_declined_received,
-						contactName);
-			}
-		}
-		throw new IllegalArgumentException("Unknown PrivateResponse");
-	}
-
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeOutItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeOutItem.java
index b5b22203ba..d575e2a832 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeOutItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationNoticeOutItem.java
@@ -1,20 +1,11 @@
 package org.briarproject.briar.android.contact;
 
-import android.content.Context;
 import android.support.annotation.LayoutRes;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.R;
-import org.briarproject.briar.api.blog.BlogInvitationRequest;
-import org.briarproject.briar.api.blog.BlogInvitationResponse;
-import org.briarproject.briar.api.forum.ForumInvitationRequest;
-import org.briarproject.briar.api.forum.ForumInvitationResponse;
-import org.briarproject.briar.api.introduction.IntroductionRequest;
-import org.briarproject.briar.api.introduction.IntroductionResponse;
 import org.briarproject.briar.api.messaging.PrivateRequest;
 import org.briarproject.briar.api.messaging.PrivateResponse;
-import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
-import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
@@ -26,17 +17,15 @@ class ConversationNoticeOutItem extends ConversationOutItem {
 	@Nullable
 	private final String msgText;
 
-	ConversationNoticeOutItem(Context ctx, String contactName,
-			PrivateRequest r) {
-		super(r.getId(), r.getGroupId(), getText(ctx, contactName, r),
-				r.getTimestamp(), r.isSent(), r.isSeen());
+	ConversationNoticeOutItem(String text, PrivateRequest r) {
+		super(r.getId(), r.getGroupId(), text, r.getTimestamp(), r.isSent(),
+				r.isSeen());
 		this.msgText = r.getMessage();
 	}
 
-	ConversationNoticeOutItem(Context ctx, String contactName,
-			PrivateResponse r) {
-		super(r.getId(), r.getGroupId(), getText(ctx, contactName, r),
-				r.getTimestamp(), r.isSent(), r.isSeen());
+	ConversationNoticeOutItem(String text, PrivateResponse r) {
+		super(r.getId(), r.getGroupId(), text, r.getTimestamp(), r.isSent(),
+				r.isSeen());
 		this.msgText = null;
 	}
 
@@ -50,58 +39,4 @@ class ConversationNoticeOutItem extends ConversationOutItem {
 	public int getLayout() {
 		return R.layout.list_item_conversation_notice_out;
 	}
-
-	private static String getText(Context ctx, String contactName,
-			PrivateRequest r) {
-		if (r instanceof IntroductionRequest) {
-			return ctx.getString(R.string.introduction_request_sent,
-					contactName, r.getName());
-		} else if (r instanceof ForumInvitationRequest) {
-			return ctx.getString(R.string.forum_invitation_sent,
-					r.getName(), contactName);
-		} else if (r instanceof BlogInvitationRequest) {
-			return ctx.getString(R.string.blogs_sharing_invitation_sent,
-					r.getName(), contactName);
-		} else if (r instanceof GroupInvitationRequest) {
-			return ctx.getString(R.string.groups_invitations_invitation_sent,
-					contactName, r.getName());
-		}
-		throw new IllegalArgumentException("Unknown PrivateRequest");
-	}
-
-	private static String getText(Context ctx, String contactName,
-			PrivateResponse r) {
-		if (r.wasAccepted()) {
-			if (r instanceof IntroductionResponse) {
-				String name = ((IntroductionResponse) r).getIntroducedAuthor()
-						.getName();
-				return ctx.getString(
-						R.string.introduction_response_accepted_sent,
-						name) + "\n\n" + ctx.getString(
-						R.string.introduction_response_accepted_sent_info,
-						name);
-			} else if (r instanceof ForumInvitationResponse) {
-				return ctx.getString(R.string.forum_invitation_response_accepted_sent, contactName);
-			} else if (r instanceof BlogInvitationResponse) {
-				return ctx.getString(R.string.blogs_sharing_response_accepted_sent, contactName);
-			} else if (r instanceof GroupInvitationResponse) {
-				return ctx.getString(R.string.groups_invitations_response_accepted_sent, contactName);
-			}
-		} else {
-			if (r instanceof IntroductionResponse) {
-				String name = ((IntroductionResponse) r).getIntroducedAuthor()
-						.getName();
-				return ctx.getString(
-						R.string.introduction_response_declined_sent, name);
-			} else if (r instanceof ForumInvitationResponse) {
-				return ctx.getString(R.string.forum_invitation_response_declined_sent, contactName);
-			} else if (r instanceof BlogInvitationResponse) {
-				return ctx.getString(R.string.blogs_sharing_response_declined_sent, contactName);
-			} else if (r instanceof GroupInvitationResponse) {
-				return ctx.getString(R.string.groups_invitations_response_declined_sent, contactName);
-			}
-		}
-		throw new IllegalArgumentException("Unknown PrivateResponse");
-	}
-
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationRequestItem.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationRequestItem.java
index e61ebec223..f6dc0a2ead 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationRequestItem.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationRequestItem.java
@@ -1,33 +1,23 @@
 package org.briarproject.briar.android.contact;
 
-import android.content.Context;
 import android.support.annotation.LayoutRes;
 
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.briar.R;
-import org.briarproject.briar.api.blog.BlogInvitationRequest;
 import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.api.forum.ForumInvitationRequest;
-import org.briarproject.briar.api.introduction.IntroductionRequest;
 import org.briarproject.briar.api.messaging.PrivateRequest;
-import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
 import org.briarproject.briar.api.sharing.InvitationRequest;
 import org.briarproject.briar.api.sharing.Shareable;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.NotThreadSafe;
 
-import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.BLOG;
-import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.FORUM;
-import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.GROUP;
-import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.INTRODUCTION;
-
 @NotThreadSafe
 @NotNullByDefault
 class ConversationRequestItem extends ConversationNoticeInItem {
 
-	enum RequestType { INTRODUCTION, FORUM, BLOG, GROUP }
+	enum RequestType {INTRODUCTION, FORUM, BLOG, GROUP}
 
 	@Nullable
 	private final GroupId requestedGroupId;
@@ -36,11 +26,10 @@ class ConversationRequestItem extends ConversationNoticeInItem {
 	private final boolean canBeOpened;
 	private boolean answered;
 
-	ConversationRequestItem(Context ctx, String contactName,
-			PrivateRequest r) {
-		super(r.getId(), r.getGroupId(), getText(ctx, contactName, r),
-				r.getMessage(), r.getTimestamp(), r.isRead());
-		this.requestType = getType(r);
+	ConversationRequestItem(String text, RequestType type, PrivateRequest r) {
+		super(r.getId(), r.getGroupId(), text, r.getMessage(),
+				r.getTimestamp(), r.isRead());
+		this.requestType = type;
 		this.sessionId = r.getSessionId();
 		this.answered = r.wasAnswered();
 		if (r instanceof InvitationRequest) {
@@ -82,47 +71,4 @@ class ConversationRequestItem extends ConversationNoticeInItem {
 	public int getLayout() {
 		return R.layout.list_item_conversation_request;
 	}
-
-	private static String getText(Context ctx, String contactName,
-			PrivateRequest r) {
-		if (r instanceof IntroductionRequest) {
-			if (r.wasAnswered()) {
-				return ctx.getString(
-						R.string.introduction_request_answered_received,
-						contactName, r.getName());
-			} else if (((IntroductionRequest) r).isContact()) {
-				return ctx.getString(
-						R.string.introduction_request_exists_received,
-						contactName, r.getName());
-			} else {
-				return ctx.getString(R.string.introduction_request_received,
-						contactName, r.getName());
-			}
-		} else if (r instanceof ForumInvitationRequest) {
-			return ctx.getString(R.string.forum_invitation_received,
-					contactName, r.getName());
-		} else if (r instanceof BlogInvitationRequest) {
-			return ctx.getString(R.string.blogs_sharing_invitation_received,
-					contactName, r.getName());
-		} else if (r instanceof GroupInvitationRequest) {
-			return ctx.getString(
-					R.string.groups_invitations_invitation_received,
-					contactName, r.getName());
-		}
-		throw new IllegalArgumentException("Unknown PrivateRequest");
-	}
-
-	private static RequestType getType(PrivateRequest r) {
-		if (r instanceof IntroductionRequest) {
-			return INTRODUCTION;
-		} else if (r instanceof ForumInvitationRequest) {
-			return FORUM;
-		} else if (r instanceof BlogInvitationRequest) {
-			return BLOG;
-		} else if (r instanceof GroupInvitationRequest) {
-			return GROUP;
-		}
-		throw new IllegalArgumentException("Unknown PrivateRequest");
-	}
-
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationVisitor.java
new file mode 100644
index 0000000000..e74dfecf30
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationVisitor.java
@@ -0,0 +1,246 @@
+package org.briarproject.briar.android.contact;
+
+import android.arch.lifecycle.LiveData;
+import android.content.Context;
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.R;
+import org.briarproject.briar.api.blog.BlogInvitationRequest;
+import org.briarproject.briar.api.blog.BlogInvitationResponse;
+import org.briarproject.briar.api.forum.ForumInvitationRequest;
+import org.briarproject.briar.api.forum.ForumInvitationResponse;
+import org.briarproject.briar.api.introduction.IntroductionRequest;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.messaging.PrivateMessageHeader;
+import org.briarproject.briar.api.messaging.PrivateMessageVisitor;
+import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest;
+import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
+
+import javax.annotation.Nullable;
+
+import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.BLOG;
+import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.FORUM;
+import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.GROUP;
+import static org.briarproject.briar.android.contact.ConversationRequestItem.RequestType.INTRODUCTION;
+
+@UiThread
+@NotNullByDefault
+class ConversationVisitor implements PrivateMessageVisitor<ConversationItem> {
+
+	private final Context ctx;
+	private final BodyCache bodyCache;
+	private final LiveData<String> contactName;
+
+	ConversationVisitor(Context ctx, BodyCache bodyCache,
+			LiveData<String> contactName) {
+		this.ctx = ctx;
+		this.bodyCache = bodyCache;
+		this.contactName = contactName;
+	}
+
+	@Override
+	public ConversationItem visitPrivateMessageHeader(PrivateMessageHeader h) {
+		ConversationItem item;
+		if (h.isLocal()) item = new ConversationMessageOutItem(h);
+		else item = new ConversationMessageInItem(h);
+		String body = bodyCache.getBody(h.getId());
+		if (body != null) item.setBody(body);
+		return item;
+	}
+
+	@Override
+	public ConversationItem visitBlogInvitationRequest(
+			BlogInvitationRequest r) {
+		if (r.isLocal()) {
+			String text = ctx.getString(R.string.blogs_sharing_invitation_sent,
+					r.getName(), contactName.getValue());
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text = ctx.getString(
+					R.string.blogs_sharing_invitation_received,
+					contactName.getValue(), r.getName());
+			return new ConversationRequestItem(text, BLOG, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitBlogInvitationResponse(
+			BlogInvitationResponse r) {
+		if (r.isLocal()) {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.blogs_sharing_response_accepted_sent,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.blogs_sharing_response_declined_sent,
+						contactName.getValue());
+			}
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.blogs_sharing_response_accepted_received,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.blogs_sharing_response_declined_received,
+						contactName.getValue());
+			}
+			return new ConversationNoticeInItem(text, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitForumInvitationRequest(
+			ForumInvitationRequest r) {
+		if (r.isLocal()) {
+			String text = ctx.getString(R.string.forum_invitation_sent,
+					r.getName(), contactName.getValue());
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text = ctx.getString(
+					R.string.forum_invitation_received,
+					contactName.getValue(), r.getName());
+			return new ConversationRequestItem(text, FORUM, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitForumInvitationResponse(
+			ForumInvitationResponse r) {
+		if (r.isLocal()) {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.forum_invitation_response_accepted_sent,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.forum_invitation_response_declined_sent,
+						contactName.getValue());
+			}
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.forum_invitation_response_accepted_received,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.forum_invitation_response_declined_received,
+						contactName.getValue());
+			}
+			return new ConversationNoticeInItem(text, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitGroupInvitationRequest(
+			GroupInvitationRequest r) {
+		if (r.isLocal()) {
+			String text = ctx.getString(
+					R.string.groups_invitations_invitation_sent,
+					contactName.getValue(), r.getName());
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text = ctx.getString(
+					R.string.groups_invitations_invitation_received,
+					contactName.getValue(), r.getName());
+			return new ConversationRequestItem(text, GROUP, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitGroupInvitationResponse(
+			GroupInvitationResponse r) {
+		if (r.isLocal()) {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.groups_invitations_response_accepted_sent,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.groups_invitations_response_declined_sent,
+						contactName.getValue());
+			}
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.groups_invitations_response_accepted_received,
+						contactName.getValue());
+			} else {
+				text = ctx.getString(
+						R.string.groups_invitations_response_declined_received,
+						contactName.getValue());
+			}
+			return new ConversationNoticeInItem(text, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitIntroductionRequest(IntroductionRequest r) {
+		if (r.isLocal()) {
+			String text = ctx.getString(R.string.introduction_request_sent,
+					contactName.getValue(), r.getName());
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text = ctx.getString(R.string.introduction_request_received,
+					contactName.getValue(), r.getName());
+			return new ConversationRequestItem(text, INTRODUCTION, r);
+		}
+	}
+
+	@Override
+	public ConversationItem visitIntroductionResponse(IntroductionResponse r) {
+		if (r.isLocal()) {
+			String text;
+			if (r.wasAccepted()) {
+				String introducee = r.getIntroducedAuthor().getName();
+				text = ctx.getString(
+						R.string.introduction_response_accepted_sent,
+						introducee)
+						+ "\n\n" + ctx.getString(
+						R.string.introduction_response_accepted_sent_info,
+						introducee);
+			} else {
+				text = ctx.getString(
+						R.string.introduction_response_declined_sent,
+						r.getIntroducedAuthor().getName());
+			}
+			return new ConversationNoticeOutItem(text, r);
+		} else {
+			String text;
+			if (r.wasAccepted()) {
+				text = ctx.getString(
+						R.string.introduction_response_accepted_received,
+						contactName.getValue(),
+						r.getIntroducedAuthor().getName());
+			} else if (r.isIntroducer()) {
+				text = ctx.getString(
+						R.string.introduction_response_declined_received,
+						contactName.getValue(),
+						r.getIntroducedAuthor().getName());
+			} else {
+				text = ctx.getString(
+						R.string.introduction_response_declined_received_by_introducee,
+						contactName.getValue(),
+						r.getIntroducedAuthor().getName());
+			}
+			return new ConversationNoticeInItem(text, r);
+		}
+	}
+
+	interface BodyCache {
+		@Nullable
+		String getBody(MessageId m);
+	}
+}
-- 
GitLab